From 875b07fb3a2a975e85155cdc5a0bd62b76330d73 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 11 Apr 2026 08:04:47 +0200 Subject: [PATCH] feat: use FileInput DS component for file uploads, fix certs volume perms - Replace inline FileField and native 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) --- docker-compose.yml | 2 + ui/package-lock.json | 8 +-- ui/package.json | 2 +- ui/src/pages/tenant/SsoPage.tsx | 28 +++------- ui/src/pages/vendor/CertificatesPage.tsx | 70 ++++++++---------------- 5 files changed, 39 insertions(+), 71 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 57b90ea..7eb49c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,6 +51,8 @@ services: {"subject":"$$SUBJECT","fingerprint":"$$FINGERPRINT","selfSigned":$$SELF_SIGNED,"hasCa":$$HAS_CA,"notBefore":"$$NOT_BEFORE","notAfter":"$$NOT_AFTER"} METAEOF mkdir -p /certs/staged /certs/prev + chmod 775 /certs /certs/staged /certs/prev + chmod 660 /certs/*.pem 2>/dev/null || true environment: PUBLIC_HOST: ${PUBLIC_HOST:-localhost} CERT_FILE: ${CERT_FILE:-} diff --git a/ui/package-lock.json b/ui/package-lock.json index 2815445..7e82179 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { - "@cameleer/design-system": "^0.1.42", + "@cameleer/design-system": "^0.0.0-snapshot.20260411.fd08d7a", "@logto/react": "^4.0.13", "@tanstack/react-query": "^5.90.0", "lucide-react": "^1.7.0", @@ -309,9 +309,9 @@ } }, "node_modules/@cameleer/design-system": { - "version": "0.1.42", - "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.42/design-system-0.1.42.tgz", - "integrity": "sha512-Cyy+2HsbBPLKRZaSGFxMUvIwI+g8ocdjcojFTGgtq5vrpE/8IYJLgxdtM9+eDoF2Zewk7MrBzfpNqjEYlQO3ng==", + "version": "0.0.0-snapshot.20260411.fd08d7a", + "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.0.0-snapshot.20260411.fd08d7a/design-system-0.0.0-snapshot.20260411.fd08d7a.tgz", + "integrity": "sha512-ehTXpF6U7bZpvqwj/aaZxdD75sDB8zpn1QVQMDzVUmQGH6RMf9O261DiV1agsOSqPNUB9UPVcdzevMo8ooWTFg==", "dependencies": { "lucide-react": "^1.7.0", "react": "^19.0.0", diff --git a/ui/package.json b/ui/package.json index b556d3c..30efd06 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,7 +10,7 @@ "postinstall": "node -e \"const fs=require('fs'),p='node_modules/@cameleer/design-system/assets/';if(fs.existsSync('public')){fs.copyFileSync(p+'cameleer3-logo.svg','public/favicon.svg')}\"" }, "dependencies": { - "@cameleer/design-system": "^0.1.42", + "@cameleer/design-system": "^0.0.0-snapshot.20260411.fd08d7a", "@logto/react": "^4.0.13", "@tanstack/react-query": "^5.90.0", "lucide-react": "^1.7.0", diff --git a/ui/src/pages/tenant/SsoPage.tsx b/ui/src/pages/tenant/SsoPage.tsx index d1e9ed7..3d4ec8e 100644 --- a/ui/src/pages/tenant/SsoPage.tsx +++ b/ui/src/pages/tenant/SsoPage.tsx @@ -1,9 +1,9 @@ import { useState, useRef } from 'react'; import { Alert, AlertDialog, Badge, Button, Card, DataTable, - EmptyState, FormField, Input, Spinner, useToast, + EmptyState, FileInput, FormField, Input, Spinner, useToast, } from '@cameleer/design-system'; -import type { Column } from '@cameleer/design-system'; +import type { Column, FileInputHandle } from '@cameleer/design-system'; import { Shield, Plus, Trash2, FlaskConical, Upload, ShieldCheck } from 'lucide-react'; import { useSsoConnectors, useCreateSsoConnector, useDeleteSsoConnector, useTestSsoConnector, @@ -331,12 +331,12 @@ function CaCertificatesSection() { const deleteMutation = useDeleteCaCert(); const { toast } = useToast(); - const certInputRef = useRef(null); + const certRef = useRef(null); const [label, setLabel] = useState(''); const [deleteTarget, setDeleteTarget] = useState(null); async function handleUpload() { - const file = certInputRef.current?.files?.[0]; + const file = certRef.current?.file; if (!file) { toast({ title: 'Select a CA certificate file', variant: 'error' }); return; @@ -348,7 +348,7 @@ function CaCertificatesSection() { try { await stageMutation.mutateAsync(formData); toast({ title: 'CA certificate staged', variant: 'success' }); - if (certInputRef.current) certInputRef.current.value = ''; + certRef.current?.clear(); setLabel(''); } catch (err) { toast({ title: 'Upload failed', description: String(err), variant: 'error' }); @@ -409,21 +409,9 @@ function CaCertificatesSection() { onChange={(e) => setLabel(e.target.value)} /> - - - CA Certificate (PEM) * - - - + + } /> + Stage Certificate diff --git a/ui/src/pages/vendor/CertificatesPage.tsx b/ui/src/pages/vendor/CertificatesPage.tsx index 81ad355..738a346 100644 --- a/ui/src/pages/vendor/CertificatesPage.tsx +++ b/ui/src/pages/vendor/CertificatesPage.tsx @@ -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; - accept: string; -}) { - return ( - - - {label} - - - - ); -} - 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(null); - const keyInputRef = useRef(null); - const caInputRef = useRef(null); + const certRef = useRef(null); + const keyRef = useRef(null); + const caRef = useRef(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 */} - - - - - Key Password (if encrypted) - + + } /> + + + } /> + + setKeyPassword(e.target.value)} /> - - + + + } /> +