rekordbox-viewer/packages/frontend/src/pages/StorageConfiguration.tsx
Geert Rademakes 218046ec4f webdav setup
2025-09-17 10:55:35 +02:00

703 lines
24 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
Box,
VStack,
HStack,
Text,
Heading,
FormControl,
FormLabel,
Input,
Button,
useToast,
Alert,
AlertIcon,
AlertTitle,
AlertDescription,
Card,
CardBody,
CardHeader,
Spinner,
Badge,
Icon,
Switch,
FormHelperText,
Select,
Divider,
} from '@chakra-ui/react';
import { FiSettings, FiZap, FiSave, FiCloud, FiServer } from 'react-icons/fi';
interface S3Config {
provider: 's3';
endpoint: string;
region: string;
accessKeyId: string;
secretAccessKey: string;
bucketName: string;
useSSL: boolean;
}
interface WebDAVConfig {
provider: 'webdav';
url: string;
username: string;
password: string;
basePath?: string;
}
type StorageConfig = S3Config | WebDAVConfig;
interface TestResult {
success: boolean;
message: string;
details?: any;
provider?: string;
}
export const StorageConfiguration: React.FC = () => {
const [config, setConfig] = useState<StorageConfig>({
provider: 's3',
endpoint: '',
region: 'us-east-1',
accessKeyId: '',
secretAccessKey: '',
bucketName: '',
useSSL: true,
});
const [isLoading, setIsLoading] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [testResult, setTestResult] = useState<TestResult | null>(null);
const [currentConfig, setCurrentConfig] = useState<StorageConfig | null>(null);
const toast = useToast();
// Load current configuration on component mount
useEffect(() => {
loadCurrentConfig();
}, []);
const loadCurrentConfig = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/config/storage');
if (response.ok) {
const data = await response.json();
setCurrentConfig(data.config);
// Handle masked passwords - don't set masked values as initial state
const configWithEmptyPasswords = { ...data.config };
if (configWithEmptyPasswords.provider === 'webdav' && configWithEmptyPasswords.password === '***') {
configWithEmptyPasswords.password = '';
}
if (configWithEmptyPasswords.provider === 's3') {
if (configWithEmptyPasswords.accessKeyId === '***') {
configWithEmptyPasswords.accessKeyId = '';
}
if (configWithEmptyPasswords.secretAccessKey === '***') {
configWithEmptyPasswords.secretAccessKey = '';
}
}
setConfig(configWithEmptyPasswords);
}
} catch (error) {
console.error('Error loading storage config:', error);
} finally {
setIsLoading(false);
}
};
const handleProviderChange = (provider: 's3' | 'webdav') => {
if (provider === 's3') {
setConfig({
provider: 's3',
endpoint: '',
region: 'us-east-1',
accessKeyId: '',
secretAccessKey: '',
bucketName: '',
useSSL: true,
});
} else {
setConfig({
provider: 'webdav',
url: '',
username: '',
password: '',
basePath: '/music-files',
});
}
};
const handleInputChange = (field: string, value: string | boolean) => {
setConfig(prev => ({
...prev,
[field]: value,
}));
};
const testConnection = async () => {
setIsTesting(true);
setTestResult(null);
try {
// For testing, merge current form state with existing config to preserve passwords
const testConfig = { ...currentConfig, ...config };
const response = await fetch('/api/config/storage/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(testConfig),
});
const result = await response.json();
if (response.ok) {
setTestResult({
success: true,
message: 'Connection successful!',
details: result,
provider: testConfig.provider,
});
toast({
title: 'Connection Test Successful',
description: `${testConfig.provider.toUpperCase()} connection is working properly`,
status: 'success',
duration: 5000,
isClosable: true,
});
} else {
setTestResult({
success: false,
message: result.error || 'Connection failed',
details: result,
provider: testConfig.provider,
});
toast({
title: 'Connection Test Failed',
description: result.error || `Failed to connect to ${testConfig.provider.toUpperCase()}`,
status: 'error',
duration: 5000,
isClosable: true,
});
}
} catch (error) {
console.error('Error testing storage connection:', error);
setTestResult({
success: false,
message: 'Network error or server unavailable',
provider: testConfig.provider,
});
toast({
title: 'Connection Test Failed',
description: 'Network error or server unavailable',
status: 'error',
duration: 5000,
isClosable: true,
});
} finally {
setIsTesting(false);
}
};
const saveConfiguration = async () => {
setIsSaving(true);
try {
// Always send the complete configuration
const configToSave = { ...config };
const response = await fetch('/api/config/storage', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(configToSave),
});
if (response.ok) {
setCurrentConfig(config);
toast({
title: 'Configuration Saved',
description: `${config.provider.toUpperCase()} configuration has been saved successfully`,
status: 'success',
duration: 3000,
isClosable: true,
});
} else {
const error = await response.json();
throw new Error(error.error || 'Failed to save configuration');
}
} catch (error) {
console.error('Error saving storage config:', error);
toast({
title: 'Save Failed',
description: error instanceof Error ? error.message : 'Failed to save configuration',
status: 'error',
duration: 5000,
isClosable: true,
});
} finally {
setIsSaving(false);
}
};
const hasChanges = () => {
if (!currentConfig) return true;
// Create a copy of currentConfig with masked values replaced with empty strings for comparison
const normalizedCurrentConfig = { ...currentConfig };
if (normalizedCurrentConfig.provider === 'webdav' && normalizedCurrentConfig.password === '***') {
normalizedCurrentConfig.password = '';
}
if (normalizedCurrentConfig.provider === 's3') {
if (normalizedCurrentConfig.accessKeyId === '***') {
normalizedCurrentConfig.accessKeyId = '';
}
if (normalizedCurrentConfig.secretAccessKey === '***') {
normalizedCurrentConfig.secretAccessKey = '';
}
}
return JSON.stringify(config) !== JSON.stringify(normalizedCurrentConfig);
};
const renderS3Config = () => (
<VStack spacing={6} align="stretch">
{/* Endpoint */}
<FormControl>
<FormLabel color="white">S3 Endpoint</FormLabel>
<Input
value={config.provider === 's3' ? config.endpoint : ''}
onChange={(e) => handleInputChange('endpoint', e.target.value)}
placeholder="https://s3.amazonaws.com or http://localhost:9000 for MinIO"
bg="gray.700"
borderColor="gray.600"
color="white"
_placeholder={{ color: 'gray.400' }}
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
/>
<FormHelperText color="gray.400">
For AWS S3, use: https://s3.amazonaws.com. For MinIO: http://localhost:9000
</FormHelperText>
</FormControl>
{/* Region */}
<FormControl>
<FormLabel color="white">Region</FormLabel>
<Input
value={config.provider === 's3' ? config.region : ''}
onChange={(e) => handleInputChange('region', e.target.value)}
placeholder="us-east-1"
bg="gray.700"
borderColor="gray.600"
color="white"
_placeholder={{ color: 'gray.400' }}
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
/>
<FormHelperText color="gray.400">
AWS region (e.g., us-east-1, eu-west-1) or 'us-east-1' for MinIO
</FormHelperText>
</FormControl>
{/* Access Key */}
<FormControl>
<FormLabel color="white">Access Key ID</FormLabel>
<Input
value={config.provider === 's3' ? config.accessKeyId : ''}
onChange={(e) => handleInputChange('accessKeyId', e.target.value)}
placeholder="Your S3 access key"
bg="gray.700"
borderColor="gray.600"
color="white"
_placeholder={{ color: 'gray.400' }}
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
/>
</FormControl>
{/* Secret Key */}
<FormControl>
<FormLabel color="white">Secret Access Key</FormLabel>
<Input
type="password"
value={config.provider === 's3' ? config.secretAccessKey : ''}
onChange={(e) => handleInputChange('secretAccessKey', e.target.value)}
placeholder="Your S3 secret key"
bg="gray.700"
borderColor="gray.600"
color="white"
_placeholder={{ color: 'gray.400' }}
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
/>
</FormControl>
{/* Bucket Name */}
<FormControl>
<FormLabel color="white">Bucket Name</FormLabel>
<Input
value={config.provider === 's3' ? config.bucketName : ''}
onChange={(e) => handleInputChange('bucketName', e.target.value)}
placeholder="music-files"
bg="gray.700"
borderColor="gray.600"
color="white"
_placeholder={{ color: 'gray.400' }}
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
/>
<FormHelperText color="gray.400">
The S3 bucket where music files will be stored
</FormHelperText>
</FormControl>
{/* Use SSL */}
<FormControl display="flex" alignItems="center">
<FormLabel htmlFor="use-ssl" mb="0" color="white">
Use SSL/TLS
</FormLabel>
<Switch
id="use-ssl"
isChecked={config.provider === 's3' ? config.useSSL : false}
onChange={(e) => handleInputChange('useSSL', e.target.checked)}
colorScheme="blue"
/>
<FormHelperText color="gray.400" ml={3}>
Enable for HTTPS connections (recommended for production)
</FormHelperText>
</FormControl>
</VStack>
);
const renderWebDAVConfig = () => (
<VStack spacing={6} align="stretch">
{/* URL */}
<FormControl>
<FormLabel color="white">WebDAV URL</FormLabel>
<Input
value={config.provider === 'webdav' ? config.url : ''}
onChange={(e) => handleInputChange('url', e.target.value)}
placeholder="https://your-nextcloud.com/remote.php/dav/files/username/"
bg="gray.700"
borderColor="gray.600"
color="white"
_placeholder={{ color: 'gray.400' }}
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
/>
<FormHelperText color="gray.400">
Your Nextcloud WebDAV URL (usually ends with /remote.php/dav/files/username/)
</FormHelperText>
</FormControl>
{/* Username */}
<FormControl>
<FormLabel color="white">Username</FormLabel>
<Input
value={config.provider === 'webdav' ? config.username : ''}
onChange={(e) => handleInputChange('username', e.target.value)}
placeholder="Your Nextcloud username"
bg="gray.700"
borderColor="gray.600"
color="white"
_placeholder={{ color: 'gray.400' }}
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
/>
</FormControl>
{/* Password */}
<FormControl>
<FormLabel color="white">Password</FormLabel>
<Input
type="password"
value={config.provider === 'webdav' ? config.password : ''}
onChange={(e) => handleInputChange('password', e.target.value)}
placeholder="Your Nextcloud password or app password"
bg="gray.700"
borderColor="gray.600"
color="white"
_placeholder={{ color: 'gray.400' }}
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
/>
<FormHelperText color="gray.400">
Use your Nextcloud password or create an app password for better security
</FormHelperText>
</FormControl>
{/* Base Path */}
<FormControl>
<FormLabel color="white">Base Path (Optional)</FormLabel>
<Input
value={config.provider === 'webdav' ? config.basePath || '' : ''}
onChange={(e) => handleInputChange('basePath', e.target.value)}
placeholder="/music-files"
bg="gray.700"
borderColor="gray.600"
color="white"
_placeholder={{ color: 'gray.400' }}
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
/>
<FormHelperText color="gray.400">
Subfolder within your WebDAV storage where music files will be stored
</FormHelperText>
</FormControl>
</VStack>
);
if (isLoading) {
return (
<Box p={8}>
<VStack spacing={4} align="center">
<Spinner size="xl" />
<Text>Loading storage configuration...</Text>
</VStack>
</Box>
);
}
return (
<Box p={8} maxW="800px" mx="auto">
<VStack spacing={8} align="stretch">
{/* Header */}
<VStack spacing={2} align="start">
<HStack spacing={3}>
<Icon as={FiSettings} w={6} h={6} color="blue.400" />
<Heading size="lg" color="white">Storage Configuration</Heading>
</HStack>
<Text color="gray.400">
Configure your storage provider for music file storage and playback. Choose between S3-compatible storage or WebDAV (Nextcloud).
</Text>
</VStack>
{/* Provider Selection */}
<Card bg="gray.800" borderColor="gray.700">
<CardHeader>
<Heading size="md" color="white">Storage Provider</Heading>
</CardHeader>
<CardBody>
<VStack spacing={4} align="stretch">
<FormControl>
<FormLabel color="white">Select Storage Provider</FormLabel>
<Select
value={config.provider}
onChange={(e) => handleProviderChange(e.target.value as 's3' | 'webdav')}
bg="gray.700"
borderColor="gray.600"
color="white"
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
>
<option value="s3">S3-Compatible Storage (AWS S3, MinIO, etc.)</option>
<option value="webdav">WebDAV (Nextcloud, ownCloud, etc.)</option>
</Select>
</FormControl>
<HStack spacing={4} align="center">
<Icon
as={config.provider === 's3' ? FiCloud : FiServer}
w={5} h={5}
color={config.provider === 's3' ? 'blue.400' : 'green.400'}
/>
<Text color="gray.400">
{config.provider === 's3'
? 'S3-compatible storage for scalable cloud storage'
: 'WebDAV for self-hosted solutions like Nextcloud'
}
</Text>
</HStack>
</VStack>
</CardBody>
</Card>
{/* Configuration Form */}
<Card bg="gray.800" borderColor="gray.700">
<CardHeader>
<Heading size="md" color="white">
{config.provider === 's3' ? 'S3 Configuration' : 'WebDAV Configuration'}
</Heading>
</CardHeader>
<CardBody>
{config.provider === 's3' ? renderS3Config() : renderWebDAVConfig()}
</CardBody>
</Card>
{/* Test Connection */}
<Card bg="gray.800" borderColor="gray.700">
<CardHeader>
<HStack spacing={3}>
<Icon as={FiZap} w={5} h={5} color="blue.400" />
<Heading size="md" color="white">Test Connection</Heading>
</HStack>
</CardHeader>
<CardBody>
<VStack spacing={4} align="stretch">
<Text color="gray.400">
Test your {config.provider.toUpperCase()} configuration to ensure it's working properly before saving.
</Text>
<Button
leftIcon={isTesting ? <Spinner size="sm" /> : <FiZap />}
onClick={testConnection}
isLoading={isTesting}
loadingText="Testing..."
colorScheme="blue"
_hover={{ bg: "blue.700" }}
>
Test Connection
</Button>
{testResult && (
<Alert
status={testResult.success ? 'success' : 'error'}
variant="subtle"
flexDirection="column"
alignItems="center"
justifyContent="center"
textAlign="center"
height="auto"
py={4}
>
<AlertIcon boxSize="24px" />
<AlertTitle mt={2} mb={1} fontSize="lg">
{testResult.success ? 'Connection Successful' : 'Connection Failed'}
</AlertTitle>
<AlertDescription maxWidth="sm">
{testResult.message}
</AlertDescription>
{testResult.details && (
<Box mt={2} p={3} bg="gray.700" borderRadius="md" fontSize="sm">
<Text color="gray.300" fontWeight="bold">Details:</Text>
<Text color="gray.400" fontFamily="mono">
{JSON.stringify(testResult.details, null, 2)}
</Text>
</Box>
)}
</Alert>
)}
</VStack>
</CardBody>
</Card>
{/* Save Configuration */}
<Card bg="gray.800" borderColor="gray.700">
<CardHeader>
<HStack spacing={3}>
<Icon as={FiSave} w={5} h={5} color="green.400" />
<Heading size="md" color="white">Save Configuration</Heading>
</HStack>
</CardHeader>
<CardBody>
<VStack spacing={4} align="stretch">
<Text color="gray.400">
Save your {config.provider.toUpperCase()} configuration to use it for music file storage and playback.
</Text>
<HStack spacing={3}>
<Button
leftIcon={isSaving ? <Spinner size="sm" /> : <FiSave />}
onClick={saveConfiguration}
isLoading={isSaving}
loadingText="Saving..."
colorScheme="green"
isDisabled={!hasChanges()}
_hover={{ bg: "green.700" }}
>
Save Configuration
</Button>
{currentConfig && (
<Badge colorScheme="blue" variant="subtle" bg="blue.900" color="blue.200">
Configuration Loaded
</Badge>
)}
</HStack>
{!hasChanges() && currentConfig && (
<Alert status="info" variant="subtle">
<AlertIcon />
<Text color="gray.300">No changes to save</Text>
</Alert>
)}
</VStack>
</CardBody>
</Card>
{/* Help Section */}
<Card bg="gray.800" borderColor="gray.700">
<CardHeader>
<Heading size="md" color="white">Configuration Help</Heading>
</CardHeader>
<CardBody>
<VStack spacing={6} align="stretch">
{/* S3 Help */}
<Box>
<Text color="gray.400" fontWeight="bold" mb={2}>
<Icon as={FiCloud} w={4} h={4} mr={2} />
S3-Compatible Storage
</Text>
<Box pl={4} borderLeft="2px" borderColor="gray.600">
<Text color="gray.400" fontSize="sm" mb={2}>
<strong>For AWS S3:</strong>
</Text>
<Text color="gray.400" fontSize="sm" pl={2}>
• Endpoint: https://s3.amazonaws.com<br/>
• Region: Your AWS region (e.g., us-east-1)<br/>
• Access Key: Your AWS access key<br/>
• Secret Key: Your AWS secret key<br/>
• Bucket: Your S3 bucket name
</Text>
<Text color="gray.400" fontSize="sm" mt={3} mb={2}>
<strong>For MinIO (Local Development):</strong>
</Text>
<Text color="gray.400" fontSize="sm" pl={2}>
• Endpoint: http://localhost:9000<br/>
• Region: us-east-1<br/>
• Access Key: minioadmin<br/>
• Secret Key: minioadmin<br/>
• Bucket: Create a bucket named 'music-files'
</Text>
</Box>
</Box>
<Divider borderColor="gray.600" />
{/* WebDAV Help */}
<Box>
<Text color="gray.400" fontWeight="bold" mb={2}>
<Icon as={FiServer} w={4} h={4} mr={2} />
WebDAV (Nextcloud/ownCloud)
</Text>
<Box pl={4} borderLeft="2px" borderColor="gray.600">
<Text color="gray.400" fontSize="sm" mb={2}>
<strong>For Nextcloud:</strong>
</Text>
<Text color="gray.400" fontSize="sm" pl={2}>
URL: https://your-nextcloud.com/remote.php/dav/files/username/<br/>
Username: Your Nextcloud username<br/>
Password: Your Nextcloud password or app password<br/>
Base Path: /music-files (optional subfolder)
</Text>
<Text color="gray.400" fontSize="sm" mt={3} mb={2}>
<strong>For ownCloud:</strong>
</Text>
<Text color="gray.400" fontSize="sm" pl={2}>
URL: https://your-owncloud.com/remote.php/dav/files/username/<br/>
Username: Your ownCloud username<br/>
Password: Your ownCloud password or app password<br/>
Base Path: /music-files (optional subfolder)
</Text>
<Text color="gray.400" fontSize="sm" mt={3}>
<strong>Note:</strong> For better security, create an app password in your Nextcloud/ownCloud settings instead of using your main password.
</Text>
</Box>
</Box>
</VStack>
</CardBody>
</Card>
</VStack>
</Box>
);
};