Prompt for upload re-auth on demand
This commit is contained in:
+105
-17
@@ -3,8 +3,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import AutoTextarea from '@/components/AutoTextarea';
|
import AutoTextarea from '@/components/AutoTextarea';
|
||||||
|
import { StorageSessionPrompt } from '@/components/StorageSessionPrompt';
|
||||||
import { useToast } from '@/lib/contexts/ToastContext';
|
import { useToast } from '@/lib/contexts/ToastContext';
|
||||||
import { useAccentColor } from '@/lib/contexts/AccentColorContext';
|
import { useAccentColor } from '@/lib/contexts/AccentColorContext';
|
||||||
|
import { refreshStorageSession } from '@/lib/storage/client';
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
@@ -27,6 +29,11 @@ export default function AdminPage() {
|
|||||||
const [savingSettings, setSavingSettings] = useState(false);
|
const [savingSettings, setSavingSettings] = useState(false);
|
||||||
const [isUploadingBanner, setIsUploadingBanner] = useState(false);
|
const [isUploadingBanner, setIsUploadingBanner] = useState(false);
|
||||||
const [bannerUploadError, setBannerUploadError] = useState<string | null>(null);
|
const [bannerUploadError, setBannerUploadError] = useState<string | null>(null);
|
||||||
|
const [showBannerSessionPrompt, setShowBannerSessionPrompt] = useState(false);
|
||||||
|
const [pendingBannerFile, setPendingBannerFile] = useState<File | null>(null);
|
||||||
|
const [bannerPassword, setBannerPassword] = useState('');
|
||||||
|
const [bannerPromptError, setBannerPromptError] = useState('');
|
||||||
|
const [isRefreshingBannerSession, setIsRefreshingBannerSession] = useState(false);
|
||||||
const [isUploadingLogo, setIsUploadingLogo] = useState(false);
|
const [isUploadingLogo, setIsUploadingLogo] = useState(false);
|
||||||
const [logoUploadError, setLogoUploadError] = useState<string | null>(null);
|
const [logoUploadError, setLogoUploadError] = useState<string | null>(null);
|
||||||
const [isUploadingFavicon, setIsUploadingFavicon] = useState(false);
|
const [isUploadingFavicon, setIsUploadingFavicon] = useState(false);
|
||||||
@@ -92,11 +99,7 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBannerUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const uploadBannerFile = async (file: File, allowPrompt = true) => {
|
||||||
const file = event.target.files?.[0];
|
|
||||||
event.target.value = '';
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
setBannerUploadError(null);
|
setBannerUploadError(null);
|
||||||
setIsUploadingBanner(true);
|
setIsUploadingBanner(true);
|
||||||
|
|
||||||
@@ -110,6 +113,13 @@ export default function AdminPage() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (!res.ok || !data.url) {
|
if (!res.ok || !data.url) {
|
||||||
|
if (res.status === 401 && allowPrompt) {
|
||||||
|
setPendingBannerFile(file);
|
||||||
|
setBannerPromptError('');
|
||||||
|
setShowBannerSessionPrompt(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(data.error || 'Upload failed');
|
throw new Error(data.error || 'Upload failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +129,10 @@ export default function AdminPage() {
|
|||||||
};
|
};
|
||||||
setNodeSettings(nextSettings);
|
setNodeSettings(nextSettings);
|
||||||
await handleSaveSettings(nextSettings);
|
await handleSaveSettings(nextSettings);
|
||||||
|
setPendingBannerFile(null);
|
||||||
|
setShowBannerSessionPrompt(false);
|
||||||
|
setBannerPassword('');
|
||||||
|
setBannerPromptError('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Banner upload failed', error);
|
console.error('Banner upload failed', error);
|
||||||
setBannerUploadError(error instanceof Error ? error.message : 'Upload failed. Please try again.');
|
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<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
event.target.value = '';
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
await uploadBannerFile(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBannerSessionSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
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<HTMLInputElement>) => {
|
const handleLogoUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
@@ -382,15 +437,34 @@ export default function AdminPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{nodeSettings.bannerUrl && (
|
{nodeSettings.bannerUrl && (
|
||||||
<div style={{ marginTop: '8px', height: '120px', borderRadius: '8px', overflow: 'hidden', border: '1px solid var(--border)', position: 'relative' }}>
|
<div style={{ marginTop: '12px' }}>
|
||||||
<div style={{
|
<div style={{ fontSize: '12px', color: 'var(--foreground-tertiary)', marginBottom: '8px' }}>
|
||||||
position: 'absolute', inset: 0,
|
Sidebar preview
|
||||||
background: `url(${nodeSettings.bannerUrl}) center/cover no-repeat`
|
</div>
|
||||||
}} />
|
<div
|
||||||
<div style={{
|
className="card"
|
||||||
position: 'absolute', inset: 0,
|
style={{
|
||||||
background: 'linear-gradient(to bottom, transparent, var(--background-secondary))'
|
padding: 0,
|
||||||
}} />
|
overflow: 'hidden',
|
||||||
|
maxWidth: '360px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '140px',
|
||||||
|
background: `url(${nodeSettings.bannerUrl}) center/cover no-repeat`,
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ padding: '16px' }}>
|
||||||
|
<h3 style={{ fontWeight: 600, marginBottom: '12px' }}>
|
||||||
|
Welcome to {nodeSettings.name || 'Your Node'}
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: 'var(--foreground-secondary)', fontSize: '14px', lineHeight: 1.6 }}>
|
||||||
|
{nodeSettings.description || 'A brief tagline for your node.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -530,9 +604,23 @@ export default function AdminPage() {
|
|||||||
<button className="btn btn-primary" onClick={() => handleSaveSettings()} disabled={savingSettings}>
|
<button className="btn btn-primary" onClick={() => handleSaveSettings()} disabled={savingSettings}>
|
||||||
{savingSettings ? 'Saving...' : 'Save Settings'}
|
{savingSettings ? 'Saving...' : 'Save Settings'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<StorageSessionPrompt
|
||||||
|
open={showBannerSessionPrompt}
|
||||||
|
isSubmitting={isRefreshingBannerSession}
|
||||||
|
password={bannerPassword}
|
||||||
|
error={bannerPromptError}
|
||||||
|
description="Please confirm your password to continue uploading this banner to your storage."
|
||||||
|
onPasswordChange={(nextPassword) => {
|
||||||
|
setBannerPassword(nextPassword);
|
||||||
|
setBannerPromptError('');
|
||||||
|
}}
|
||||||
|
onSubmit={handleBannerSessionSubmit}
|
||||||
|
onCancel={handleBannerSessionCancel}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<HTMLFormElement>) => 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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
background: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 99999,
|
||||||
|
padding: '20px',
|
||||||
|
}}
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card"
|
||||||
|
style={{ width: '100%', maxWidth: '420px', padding: '20px' }}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h3 style={{ fontSize: '18px', fontWeight: 600, marginBottom: '8px' }}>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: 'var(--foreground-secondary)', marginBottom: '16px', lineHeight: 1.5 }}>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<label
|
||||||
|
htmlFor="storage-session-password"
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '8px',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="storage-session-password"
|
||||||
|
type="password"
|
||||||
|
className="input"
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => onPasswordChange(event.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{ color: 'var(--error)', fontSize: '13px', marginBottom: '12px' }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
|
||||||
|
<button type="button" className="btn btn-ghost" onClick={onCancel} disabled={isSubmitting}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? 'Confirming...' : 'Continue'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
import { StorageSessionPrompt } from '@/components/StorageSessionPrompt';
|
||||||
|
import { refreshStorageSession } from '@/lib/storage/client';
|
||||||
|
|
||||||
interface UserStorageImageUploadProps {
|
interface UserStorageImageUploadProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -25,6 +27,11 @@ export function UserStorageImageUpload({
|
|||||||
}: UserStorageImageUploadProps) {
|
}: UserStorageImageUploadProps) {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [showSessionPrompt, setShowSessionPrompt] = useState(false);
|
||||||
|
const [pendingFile, setPendingFile] = useState<File | null>(null);
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [promptError, setPromptError] = useState('');
|
||||||
|
const [isRefreshingSession, setIsRefreshingSession] = useState(false);
|
||||||
|
|
||||||
const resetFileInput = () => {
|
const resetFileInput = () => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
@@ -32,7 +39,7 @@ export function UserStorageImageUpload({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFile = async (file: File) => {
|
const uploadFile = async (file: File, allowPrompt = true) => {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -48,8 +55,11 @@ export function UserStorageImageUpload({
|
|||||||
if (!res.ok || !data.url) {
|
if (!res.ok || !data.url) {
|
||||||
const message = data.error || 'Upload failed';
|
const message = data.error || 'Upload failed';
|
||||||
|
|
||||||
if (res.status === 401) {
|
if (res.status === 401 && allowPrompt) {
|
||||||
throw new Error(message || 'Upload session expired. Please sign in again.');
|
setPendingFile(file);
|
||||||
|
setPromptError('');
|
||||||
|
setShowSessionPrompt(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
@@ -57,6 +67,10 @@ export function UserStorageImageUpload({
|
|||||||
|
|
||||||
onChange(data.media?.url || data.url);
|
onChange(data.media?.url || data.url);
|
||||||
onError?.('');
|
onError?.('');
|
||||||
|
setPendingFile(null);
|
||||||
|
setShowSessionPrompt(false);
|
||||||
|
setPassword('');
|
||||||
|
setPromptError('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onError?.(error instanceof Error ? error.message : 'Upload failed');
|
onError?.(error instanceof Error ? error.message : 'Upload failed');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -71,70 +85,109 @@ export function UserStorageImageUpload({
|
|||||||
await uploadFile(file);
|
await uploadFile(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePromptSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
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 (
|
return (
|
||||||
<div>
|
<>
|
||||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: 500, marginBottom: '8px' }}>
|
<div>
|
||||||
{label}
|
<label style={{ display: 'block', fontSize: '14px', fontWeight: 500, marginBottom: '8px' }}>
|
||||||
</label>
|
{label}
|
||||||
<div style={{ display: 'flex', gap: '12px', alignItems: 'center', flexWrap: 'wrap' }}>
|
|
||||||
<label className="btn btn-ghost btn-sm" style={{ cursor: isUploading ? 'default' : 'pointer' }}>
|
|
||||||
{isUploading ? 'Uploading...' : 'Choose File'}
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
disabled={isUploading}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
|
<div style={{ display: 'flex', gap: '12px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
{value && (
|
<label className="btn btn-ghost btn-sm" style={{ cursor: isUploading ? 'default' : 'pointer' }}>
|
||||||
<div
|
{isUploading ? 'Uploading...' : 'Choose File'}
|
||||||
style={{
|
<input
|
||||||
width: `${previewWidth}px`,
|
ref={inputRef}
|
||||||
height: `${previewHeight}px`,
|
type="file"
|
||||||
borderRadius: previewBorderRadius,
|
accept="image/*"
|
||||||
overflow: 'hidden',
|
onChange={handleFileChange}
|
||||||
border: '1px solid var(--border)',
|
disabled={isUploading}
|
||||||
background: 'var(--background-tertiary)',
|
style={{ display: 'none' }}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={value}
|
|
||||||
alt={`${label} preview`}
|
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</label>
|
||||||
)}
|
|
||||||
|
|
||||||
{value && (
|
{value && (
|
||||||
<button
|
<div
|
||||||
type="button"
|
style={{
|
||||||
onClick={() => onChange('')}
|
width: `${previewWidth}px`,
|
||||||
className="btn btn-ghost btn-sm"
|
height: `${previewHeight}px`,
|
||||||
style={{ color: 'var(--error)' }}
|
borderRadius: previewBorderRadius,
|
||||||
>
|
overflow: 'hidden',
|
||||||
Remove
|
border: '1px solid var(--border)',
|
||||||
</button>
|
background: 'var(--background-tertiary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={value}
|
||||||
|
alt={`${label} preview`}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{value && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange('')}
|
||||||
|
className="btn btn-ghost btn-sm"
|
||||||
|
style={{ color: 'var(--error)' }}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{helperText && (
|
||||||
|
<p style={{ fontSize: '13px', color: 'var(--foreground-tertiary)', marginTop: '6px' }}>
|
||||||
|
{helperText}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{helperText && (
|
<StorageSessionPrompt
|
||||||
<p style={{ fontSize: '13px', color: 'var(--foreground-tertiary)', marginTop: '6px' }}>
|
open={showSessionPrompt}
|
||||||
{helperText}
|
isSubmitting={isRefreshingSession}
|
||||||
</p>
|
password={password}
|
||||||
)}
|
error={promptError}
|
||||||
|
onPasswordChange={(nextPassword) => {
|
||||||
<p
|
setPassword(nextPassword);
|
||||||
style={{
|
setPromptError('');
|
||||||
fontSize: '12px',
|
|
||||||
color: 'var(--foreground-tertiary)',
|
|
||||||
marginTop: '6px',
|
|
||||||
}}
|
}}
|
||||||
>
|
onSubmit={handlePromptSubmit}
|
||||||
If uploads stop working after a long session, sign in again to refresh your upload access.
|
onCancel={handlePromptCancel}
|
||||||
</p>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export async function refreshStorageSession(password: string): Promise<void> {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user