199 lines
6.8 KiB
TypeScript
199 lines
6.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Eye, EyeOff, HardDrive, CheckCircle2, AlertCircle } from 'lucide-react';
|
|
import { saveHetznerConfig, configureHetznerCors } from '@/actions/settings';
|
|
|
|
interface HetznerConfig {
|
|
hetzner_endpoint: string;
|
|
hetzner_access_key: string;
|
|
hetzner_secret_key: string;
|
|
hetzner_bucket_name: string;
|
|
}
|
|
|
|
interface Props {
|
|
initialConfig: HetznerConfig;
|
|
}
|
|
|
|
export function HetznerConfigForm({ initialConfig }: Props) {
|
|
const [form, setForm] = useState<HetznerConfig>(initialConfig);
|
|
const [showSecret, setShowSecret] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
const [corsLoading, setCorsLoading] = useState(false);
|
|
const [corsStatus, setCorsStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
const [corsError, setCorsError] = useState<string | null>(null);
|
|
|
|
function handleChange(key: keyof HetznerConfig) {
|
|
return (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setForm((prev) => ({ ...prev, [key]: e.target.value }));
|
|
setStatus('idle');
|
|
};
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setStatus('idle');
|
|
setErrorMsg(null);
|
|
try {
|
|
await saveHetznerConfig(form);
|
|
setStatus('success');
|
|
} catch (err: unknown) {
|
|
setStatus('error');
|
|
setErrorMsg(err instanceof Error ? err.message : 'Failed to save configuration.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleConfigureCors() {
|
|
setCorsLoading(true);
|
|
setCorsStatus('idle');
|
|
setCorsError(null);
|
|
try {
|
|
await configureHetznerCors();
|
|
setCorsStatus('success');
|
|
} catch (err: unknown) {
|
|
setCorsStatus('error');
|
|
setCorsError(err instanceof Error ? err.message : 'Failed to configure CORS.');
|
|
} finally {
|
|
setCorsLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<HardDrive className="h-4 w-4" />
|
|
Hetzner Object Storage
|
|
</CardTitle>
|
|
<CardDescription className="text-sm text-muted-foreground">
|
|
Configure the Hetzner S3-compatible bucket used for high-res deliverable storage.
|
|
These values override the environment variables and take effect immediately — no rebuild required.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit} className="space-y-4 max-w-sm">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="hetzner_endpoint">Endpoint URL</Label>
|
|
<Input
|
|
id="hetzner_endpoint"
|
|
type="url"
|
|
placeholder="https://nbg1.your-objectstorage.com"
|
|
value={form.hetzner_endpoint}
|
|
onChange={handleChange('hetzner_endpoint')}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="hetzner_bucket_name">Bucket Name</Label>
|
|
<Input
|
|
id="hetzner_bucket_name"
|
|
type="text"
|
|
placeholder="vfxreview"
|
|
value={form.hetzner_bucket_name}
|
|
onChange={handleChange('hetzner_bucket_name')}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="hetzner_access_key">Access Key</Label>
|
|
<Input
|
|
id="hetzner_access_key"
|
|
type="text"
|
|
placeholder="Access key ID"
|
|
value={form.hetzner_access_key}
|
|
onChange={handleChange('hetzner_access_key')}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="hetzner_secret_key">Secret Key</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="hetzner_secret_key"
|
|
type={showSecret ? 'text' : 'password'}
|
|
placeholder="Secret key"
|
|
value={form.hetzner_secret_key}
|
|
onChange={handleChange('hetzner_secret_key')}
|
|
autoComplete="new-password"
|
|
className="pr-10"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowSecret((v) => !v)}
|
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-white"
|
|
tabIndex={-1}
|
|
>
|
|
{showSecret ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{status === 'success' && (
|
|
<div className="flex items-center gap-2 text-sm text-emerald-400">
|
|
<CheckCircle2 className="h-4 w-4 shrink-0" />
|
|
Configuration saved — changes are live immediately.
|
|
</div>
|
|
)}
|
|
|
|
{status === 'error' && (
|
|
<div className="flex items-center gap-2 text-sm text-red-400">
|
|
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
{errorMsg}
|
|
</div>
|
|
)}
|
|
|
|
<Button type="submit" disabled={loading} size="sm">
|
|
{loading ? 'Saving…' : 'Save Configuration'}
|
|
</Button>
|
|
</form>
|
|
|
|
<div className="mt-6 border-t border-border pt-5 space-y-3">
|
|
<div>
|
|
<p className="text-sm font-medium">Batch Upload — CORS</p>
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
Required once so browsers can upload large MOV files directly to
|
|
Hetzner, bypassing the Cloudflare / Nginx size limit.
|
|
</p>
|
|
</div>
|
|
|
|
{corsStatus === 'success' && (
|
|
<div className="flex items-center gap-2 text-sm text-emerald-400">
|
|
<CheckCircle2 className="h-4 w-4 shrink-0" />
|
|
CORS policy applied — batch MOV uploads should now work.
|
|
</div>
|
|
)}
|
|
|
|
{corsStatus === 'error' && (
|
|
<div className="flex items-center gap-2 text-sm text-red-400">
|
|
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
{corsError}
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={corsLoading}
|
|
onClick={handleConfigureCors}
|
|
>
|
|
{corsLoading ? 'Applying…' : 'Configure CORS on bucket'}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|