feat: use FileInput DS component for file uploads, fix certs volume perms
All checks were successful
CI / build (push) Successful in 1m24s
CI / docker (push) Successful in 1m12s

- Replace inline FileField and native <input type="file"> with
  FileInput from @cameleer/design-system (drag-and-drop, icons, clear)
- Update CertificatesPage and SsoPage to use FileInput + FormField
- Fix /certs volume permissions (chmod 775) so cameleer user can write

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-11 08:04:47 +02:00
parent 4fdf171912
commit 875b07fb3a
5 changed files with 39 additions and 71 deletions

View File

@@ -4,11 +4,14 @@ import {
Badge,
Button,
Card,
FileInput,
FormField,
Input,
Spinner,
useToast,
} from '@cameleer/design-system';
import { Upload, ShieldCheck, RotateCcw, Trash2 } from 'lucide-react';
import type { FileInputHandle } from '@cameleer/design-system';
import { Upload, ShieldCheck, RotateCcw, Trash2, FileKey, KeyRound, ShieldPlus } from 'lucide-react';
import {
useVendorCertificates,
useStageCertificate,
@@ -101,34 +104,6 @@ function CertCard({
);
}
function FileField({ label, inputRef, accept }: {
label: string;
inputRef: React.RefObject<HTMLInputElement | null>;
accept: string;
}) {
return (
<div>
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
{label}
</label>
<input
ref={inputRef}
type="file"
accept={accept}
style={{
fontSize: 13,
color: 'var(--text-primary)',
background: 'var(--bg-inset)',
border: '1px solid var(--border)',
borderRadius: 6,
padding: '6px 8px',
width: '100%',
}}
/>
</div>
);
}
export function CertificatesPage() {
const { toast } = useToast();
const { data, isLoading, isError } = useVendorCertificates();
@@ -137,9 +112,9 @@ export function CertificatesPage() {
const restoreMutation = useRestoreCertificate();
const discardMutation = useDiscardStaged();
const certInputRef = useRef<HTMLInputElement>(null);
const keyInputRef = useRef<HTMLInputElement>(null);
const caInputRef = useRef<HTMLInputElement>(null);
const certRef = useRef<FileInputHandle>(null);
const keyRef = useRef<FileInputHandle>(null);
const caRef = useRef<FileInputHandle>(null);
const [keyPassword, setKeyPassword] = useState('');
if (isLoading) {
@@ -161,9 +136,9 @@ export function CertificatesPage() {
}
async function handleUpload() {
const certFile = certInputRef.current?.files?.[0];
const keyFile = keyInputRef.current?.files?.[0];
const caFile = caInputRef.current?.files?.[0];
const certFile = certRef.current?.file;
const keyFile = keyRef.current?.file;
const caFile = caRef.current?.file;
if (!certFile || !keyFile) {
toast({ title: 'Certificate and key files are required', variant: 'error' });
@@ -180,9 +155,9 @@ export function CertificatesPage() {
const result = await stageMutation.mutateAsync(formData);
if (result.valid) {
toast({ title: 'Certificate staged successfully', variant: 'success' });
if (certInputRef.current) certInputRef.current.value = '';
if (keyInputRef.current) keyInputRef.current.value = '';
if (caInputRef.current) caInputRef.current.value = '';
certRef.current?.clear();
keyRef.current?.clear();
caRef.current?.clear();
setKeyPassword('');
} else {
toast({ title: 'Validation failed', description: result.errors.join(', '), variant: 'error' });
@@ -287,20 +262,23 @@ export function CertificatesPage() {
{/* Upload card */}
<Card title="Upload Certificate">
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<FileField label="Certificate (PEM) *" inputRef={certInputRef} accept=".pem,.crt,.cer" />
<FileField label="Private Key (PEM) *" inputRef={keyInputRef} accept=".pem,.key" />
<div>
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
Key Password (if encrypted)
</label>
<FormField label="Certificate (PEM) *">
<FileInput ref={certRef} accept=".pem,.crt,.cer" icon={<FileKey size={16} />} />
</FormField>
<FormField label="Private Key (PEM) *">
<FileInput ref={keyRef} accept=".pem,.key" icon={<KeyRound size={16} />} />
</FormField>
<FormField label="Key Password (if encrypted)">
<Input
type="password"
placeholder="Leave empty if key is not encrypted"
value={keyPassword}
onChange={(e) => setKeyPassword(e.target.value)}
/>
</div>
<FileField label="CA Bundle (PEM, optional)" inputRef={caInputRef} accept=".pem,.crt,.cer" />
</FormField>
<FormField label="CA Bundle (PEM, optional)">
<FileInput ref={caRef} accept=".pem,.crt,.cer" icon={<ShieldPlus size={16} />} />
</FormField>
<Button
variant="primary"
onClick={handleUpload}