diff --git a/components/buckets/info.tsx b/components/buckets/info.tsx index 8a9c85c..193149e 100644 --- a/components/buckets/info.tsx +++ b/components/buckets/info.tsx @@ -12,7 +12,7 @@ import { Switch } from "@/components/ui/switch" import { Spinner } from "@/components/ui/spinner" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" -import { Field, FieldContent, FieldLabel } from "@/components/ui/field" +import { Field, FieldContent, FieldDescription, FieldLabel } from "@/components/ui/field" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" @@ -29,6 +29,7 @@ import { import { useMessage } from "@/lib/feedback/message" import { formatBytes, getBytes } from "@/lib/functions" import { detectBucketPolicy, setBucketPolicy, type BucketPolicyType } from "@/lib/bucket-policy" +import { stringifyBucketCorsConfig, validateBucketCorsJson, type BucketCorsConfiguration } from "@/lib/bucket-cors" import { cn } from "@/lib/utils" interface BucketInfoProps { @@ -69,6 +70,7 @@ export function BucketInfo({ bucketName }: BucketInfoProps) { const canDeleteBucketPolicy = canCapability("bucket.policy.delete", bucketContext) const canEditBucketPolicy = canPutBucketPolicy || canDeleteBucketPolicy const canEditEncryption = canCapability("bucket.encryption.edit", bucketContext) + const canEditCors = canCapability("bucket.cors.edit", bucketContext) const canEditTags = canCapability("bucket.tag.edit", bucketContext) const canEditVersioning = canCapability("bucket.versioning.edit", bucketContext) const canEditQuota = canCapability("bucket.quota.edit", bucketContext) @@ -77,6 +79,7 @@ export function BucketInfo({ bucketName }: BucketInfoProps) { const [policy, setPolicy] = React.useState(null) const [policyType, setPolicyType] = React.useState("private") const [encryption, setEncryption] = React.useState | null>(null) + const [corsConfig, setCorsConfig] = React.useState(null) const [tags, setTags] = React.useState([]) const [objectLock, setObjectLock] = React.useState(null) const [versioning, setVersioning] = React.useState(null) @@ -97,6 +100,10 @@ export function BucketInfo({ bucketName }: BucketInfoProps) { const [encryptFormKmsKeyId, setEncryptFormKmsKeyId] = React.useState("") const [kmsKeyOptions, setKmsKeyOptions] = React.useState<{ label: string; value: string }[]>([]) + const [showCorsModal, setShowCorsModal] = React.useState(false) + const [corsFormEnabled, setCorsFormEnabled] = React.useState(false) + const [corsFormContent, setCorsFormContent] = React.useState(stringifyBucketCorsConfig()) + const [showTagModal, setShowTagModal] = React.useState(false) const [tagFormKey, setTagFormKey] = React.useState("") const [tagFormValue, setTagFormValue] = React.useState("") @@ -113,13 +120,19 @@ export function BucketInfo({ bucketName }: BucketInfoProps) { const [retentionPeriodInput, setRetentionPeriodInput] = React.useState("") const [deleteTagIndex, setDeleteTagIndex] = React.useState(null) + const corsValidation = React.useMemo( + () => (corsFormEnabled ? validateBucketCorsJson(corsFormContent) : { config: null, error: null }), + [corsFormContent, corsFormEnabled], + ) + const fetchData = React.useCallback(async () => { if (!bucketName) return setLoading(true) try { - const [p, e, tagResp, lockResp, verResp, quotaResp] = await Promise.all([ + const [p, e, corsResp, tagResp, lockResp, verResp, quotaResp] = await Promise.all([ bucketApi.getBucketPolicy(bucketName).catch(() => null), bucketApi.getBucketEncryption(bucketName).catch(() => null), + bucketApi.getBucketCors(bucketName).catch(() => null), bucketApi.getBucketTagging(bucketName).catch(() => null), bucketApi.getObjectLockConfiguration(bucketName).catch(() => null), bucketApi.getBucketVersioning(bucketName).catch(() => null), @@ -146,6 +159,11 @@ export function BucketInfo({ bucketName }: BucketInfoProps) { } setEncryption(e as unknown as Record) + setCorsConfig( + ((corsResp as BucketCorsConfiguration | null)?.CORSRules?.length ?? 0) > 0 + ? (corsResp as BucketCorsConfiguration) + : null, + ) setTags( (tagResp as { TagSet?: Array<{ Key?: string; Value?: string }> })?.TagSet?.map((x) => ({ Key: x.Key ?? "", @@ -302,6 +320,38 @@ export function BucketInfo({ bucketName }: BucketInfoProps) { } } + const openCorsModal = () => { + if (!canEditCors) return + setCorsFormEnabled(Boolean(corsConfig?.CORSRules?.length)) + setCorsFormContent(stringifyBucketCorsConfig(corsConfig)) + setShowCorsModal(true) + } + + const submitCors = async () => { + if (!canEditCors) return + + try { + if (!corsFormEnabled) { + if (corsConfig?.CORSRules?.length) { + await bucketApi.deleteBucketCors(bucketName) + } + } else { + if (corsValidation.error || !corsValidation.config) { + message.error(corsValidation.error ?? t("Invalid CORS configuration")) + return + } + + await bucketApi.putBucketCors(bucketName, corsValidation.config) + } + + message.success(t("Edit Success")) + setShowCorsModal(false) + await fetchData() + } catch (err) { + message.error(`${t("Edit Failed")}: ${(err as Error).message}`) + } + } + const openTagModal = (index = -1) => { if (!canEditTags) return setEditingTagIndex(index) @@ -460,6 +510,9 @@ export function BucketInfo({ bucketName }: BucketInfoProps) { : policyType const encryptionLabel = parseEncryptionLabel(encryption as Parameters[0]) + const corsRules = corsConfig?.CORSRules ?? [] + const corsMethods = Array.from(new Set(corsRules.flatMap((rule) => rule.AllowedMethods ?? []))) + const corsOrigins = Array.from(new Set(corsRules.flatMap((rule) => rule.AllowedOrigins ?? []))) if (loading) { return ( @@ -508,6 +561,46 @@ export function BucketInfo({ bucketName }: BucketInfoProps) { + {/* CORS */} + + +
+ {t("Bucket CORS")} + + {corsRules.length > 0 ? t("Configured") : t("Not Enabled")} + +
+ + {canEditCors ? ( + + ) : null} + +
+ + {corsRules.length > 0 ? ( +
+
+

{t("CORS Rules")}

+

{corsRules.length}

+
+
+

{t("Allowed Methods")}

+

{corsMethods.join(", ") || "-"}

+
+
+

{t("Allowed Origins")}

+

{corsOrigins.join(", ") || "-"}

+
+
+ ) : ( + {t("No Data")} + )} +
+
+ {/* Tag */} @@ -750,6 +843,57 @@ export function BucketInfo({ bucketName }: BucketInfoProps) { + {/* CORS Modal */} + + + + {t("Set Bucket CORS")} + +
+ + {t("Bucket CORS")} + + + + + {corsFormEnabled ? ( + + {t("CORS Configuration")} + +
+