Merge 48211dd50e247bff3b9e602cbc9549ec035de468 into ee420fbf2946e9f79977615cee5e29192d7da478

This commit is contained in:
João Pedro 2025-11-07 23:10:08 -03:00 committed by GitHub
commit 8b6697455f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 550 additions and 108 deletions

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": false,
"printWidth": 80,
"tabWidth": 2
}

View File

@ -33,7 +33,7 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) {
limit = 100 limit = 100
} }
client, err := getS3Client(bucket) client, err := getS3Client(bucket, nil)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
@ -88,7 +88,7 @@ func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) {
thumbnail := queryParams.Get("thumb") == "1" thumbnail := queryParams.Get("thumb") == "1"
download := queryParams.Get("dl") == "1" download := queryParams.Get("dl") == "1"
client, err := getS3Client(bucket) client, err := getS3Client(bucket, nil)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
@ -184,7 +184,7 @@ func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) {
defer file.Close() defer file.Close()
} }
client, err := getS3Client(bucket) client, err := getS3Client(bucket, nil)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
@ -220,7 +220,7 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
recursive := r.URL.Query().Get("recursive") == "true" recursive := r.URL.Query().Get("recursive") == "true"
isDirectory := strings.HasSuffix(key, "/") isDirectory := strings.HasSuffix(key, "/")
client, err := getS3Client(bucket) client, err := getS3Client(bucket, nil)
if err != nil { if err != nil {
utils.ResponseError(w, err) utils.ResponseError(w, err)
return return
@ -284,10 +284,15 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
utils.ResponseSuccess(w, res) utils.ResponseSuccess(w, res)
} }
func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) { func getBucketCredentials(bucket string, permission *string) (aws.CredentialsProvider, error) {
cacheKey := fmt.Sprintf("key:%s", bucket) cacheKey := fmt.Sprintf("key:%s", bucket)
cacheData := utils.Cache.Get(cacheKey)
// Some CORS operations with the SDK are only achievable using a "owner" access key
if permission != nil {
cacheKey = fmt.Sprintf("key:%s:%s", *permission, bucket)
}
cacheData := utils.Cache.Get(cacheKey)
if cacheData != nil { if cacheData != nil {
return cacheData.(aws.CredentialsProvider), nil return cacheData.(aws.CredentialsProvider), nil
} }
@ -305,7 +310,10 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
var key schema.KeyElement var key schema.KeyElement
for _, k := range bucketData.Keys { for _, k := range bucketData.Keys {
if !k.Permissions.Read || !k.Permissions.Write { if permission == nil && (!k.Permissions.Read || !k.Permissions.Write){
continue
}
if permission != nil && !k.Permissions.HasPermission(*permission) {
continue continue
} }
@ -319,14 +327,18 @@ func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
break break
} }
if key.AccessKeyID == "" {
return nil, fmt.Errorf("a valid access key was not found for this bucket")
}
credential := credentials.NewStaticCredentialsProvider(key.AccessKeyID, key.SecretAccessKey, "") credential := credentials.NewStaticCredentialsProvider(key.AccessKeyID, key.SecretAccessKey, "")
utils.Cache.Set(cacheKey, credential, time.Hour) utils.Cache.Set(cacheKey, credential, time.Hour)
return credential, nil return credential, nil
} }
func getS3Client(bucket string) (*s3.Client, error) { func getS3Client(bucket string, permission *string) (*s3.Client, error) {
creds, err := getBucketCredentials(bucket) creds, err := getBucketCredentials(bucket, permission)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucket, err) return nil, fmt.Errorf("cannot get credentials for bucket %s: %w", bucket, err)
} }

View File

@ -1,11 +1,16 @@
package router package router
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"khairul169/garage-webui/schema" "khairul169/garage-webui/schema"
"khairul169/garage-webui/utils" "khairul169/garage-webui/utils"
"net/http" "net/http"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
) )
type Buckets struct{} type Buckets struct{}
@ -52,3 +57,102 @@ func (b *Buckets) GetAll(w http.ResponseWriter, r *http.Request) {
utils.ResponseSuccess(w, res) utils.ResponseSuccess(w, res)
} }
func (b*Buckets) GetCors(w http.ResponseWriter, r *http.Request){
bucket := r.PathValue("bucket")
perm := "owner"
client, err := getS3Client(bucket, &perm)
if err != nil {
utils.ResponseError(w, err)
return
}
cors, err := client.GetBucketCors(context.Background(), &s3.GetBucketCorsInput{
Bucket: aws.String(bucket),
})
if err != nil {
utils.ResponseError(w, err)
return
}
var defaultCors = []schema.BucketCors{{
AllowedOrigins: []string{},
AllowedMethods: []string{},
AllowedHeaders: []string{},
ExposeHeaders: []string{},
MaxAgeSeconds: nil,
}}
res := make([]schema.BucketCors, 0, len(cors.CORSRules))
for _, rule := range cors.CORSRules {
res = append(res, schema.BucketCors{
AllowedOrigins: rule.AllowedOrigins,
AllowedMethods: rule.AllowedMethods,
AllowedHeaders: rule.AllowedHeaders,
ExposeHeaders: rule.ExposeHeaders,
MaxAgeSeconds: rule.MaxAgeSeconds,
})
}
if len(res) == 0 {
res = defaultCors
}
utils.ResponseSuccess(w, res)
}
func (b*Buckets) PutCors(w http.ResponseWriter, r *http.Request){
bucket := r.PathValue("bucket")
var body struct {
Rules []schema.BucketCors `json:"rules"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
utils.ResponseError(w, err)
return
}
perm := "owner"
client, err := getS3Client(bucket, &perm)
if err != nil {
utils.ResponseError(w, err)
return
}
cors := make([]types.CORSRule, 0, len(body.Rules))
for _, rule := range body.Rules {
if len(rule.AllowedMethods) == 0 || len(rule.AllowedOrigins) == 0 {
utils.ResponseError(w, fmt.Errorf("each CORS rule must have at least one allowed method and origin"))
return
}
cors = append(cors, types.CORSRule{
AllowedHeaders: utils.NilIfEmpty(rule.AllowedHeaders),
AllowedMethods: rule.AllowedMethods, // required
AllowedOrigins: rule.AllowedOrigins, // required
ExposeHeaders: utils.NilIfEmpty(rule.ExposeHeaders),
MaxAgeSeconds: rule.MaxAgeSeconds,
})
}
res, err := client.PutBucketCors(context.Background(), &s3.PutBucketCorsInput{
Bucket: aws.String(bucket),
CORSConfiguration: &types.CORSConfiguration{
CORSRules: cors,
},
})
if err != nil {
utils.ResponseError(w, err)
return
}
utils.ResponseSuccess(w, res)
}

View File

@ -20,6 +20,8 @@ func HandleApiRouter() *http.ServeMux {
buckets := &Buckets{} buckets := &Buckets{}
router.HandleFunc("GET /buckets", buckets.GetAll) router.HandleFunc("GET /buckets", buckets.GetAll)
router.HandleFunc("GET /buckets/{bucket}/cors", buckets.GetCors)
router.HandleFunc("PUT /buckets/{bucket}/cors", buckets.PutCors)
browse := &Browse{} browse := &Browse{}
router.HandleFunc("GET /browse/{bucket}", browse.GetObjects) router.HandleFunc("GET /browse/{bucket}", browse.GetObjects)

View File

@ -24,6 +24,14 @@ type Bucket struct {
Created string `json:"created"` Created string `json:"created"`
} }
type BucketCors struct {
AllowedOrigins []string `json:"allowedOrigins"`
AllowedMethods []string `json:"allowedMethods"`
AllowedHeaders []string `json:"allowedHeaders"`
ExposeHeaders []string `json:"exposeHeaders"`
MaxAgeSeconds *int32 `json:"maxAgeSeconds"`
}
type LocalAlias struct { type LocalAlias struct {
AccessKeyID string `json:"accessKeyId"` AccessKeyID string `json:"accessKeyId"`
Alias string `json:"alias"` Alias string `json:"alias"`
@ -52,3 +60,16 @@ type WebsiteConfig struct {
IndexDocument string `json:"indexDocument"` IndexDocument string `json:"indexDocument"`
ErrorDocument string `json:"errorDocument"` ErrorDocument string `json:"errorDocument"`
} }
func (p *Permissions) HasPermission(perm string) bool {
switch perm {
case "read":
return p.Read
case "write":
return p.Write
case "owner":
return p.Owner
default:
return false
}
}

View File

@ -18,6 +18,13 @@ func LastString(str []string) string {
return str[len(str)-1] return str[len(str)-1]
} }
func NilIfEmpty[T any](s []T) []T {
if len(s) == 0 {
return nil
}
return s
}
func ResponseError(w http.ResponseWriter, err error) { func ResponseError(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error())) w.Write([]byte(err.Error()))

View File

@ -1,8 +1,9 @@
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import { Bucket } from "../types"; import { Bucket, BucketCors } from "../types";
export const BucketContext = createContext<{ export const BucketContext = createContext<{
bucket: Bucket; bucket: Bucket;
cors: BucketCors[];
refetch: () => void; refetch: () => void;
bucketName: string; bucketName: string;
} | null>(null); } | null>(null);

View File

@ -5,7 +5,8 @@ import {
UseMutationOptions, UseMutationOptions,
useQuery, useQuery,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { Bucket, Permissions } from "../types"; import { Bucket, BucketCors, Permissions } from "../types";
import { BucketCorsSchema } from "./schema";
export const useBucket = (id?: string | null) => { export const useBucket = (id?: string | null) => {
return useQuery({ return useQuery({
@ -110,3 +111,24 @@ export const useRemoveBucket = (
...options, ...options,
}); });
}; };
export const useBucketCors = (bucketName?: string) => {
return useQuery({
queryKey: ["bucket_cors", bucketName],
queryFn: () => api.get<BucketCors[]>(`/buckets/${bucketName}/cors`),
enabled: !!bucketName,
retry: false,
});
};
export const useBucketCorsMutation = (
options?: MutationOptions<any, Error, BucketCorsSchema>
) => {
return useMutation({
mutationFn: (data: BucketCorsSchema) =>
api.put(`/buckets/${data.bucketName}/cors`, {
body: { rules: data.rules },
}),
...options,
});
};

View File

@ -1,5 +1,5 @@
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useBucket } from "./hooks"; import { useBucket, useBucketCors } from "./hooks";
import Page from "@/context/page-context"; import Page from "@/context/page-context";
import TabView, { Tab } from "@/components/containers/tab-view"; import TabView, { Tab } from "@/components/containers/tab-view";
import { import {
@ -39,6 +39,9 @@ const tabs: Tab[] = [
const ManageBucketPage = () => { const ManageBucketPage = () => {
const { id } = useParams(); const { id } = useParams();
const { data, error, isLoading, refetch } = useBucket(id); const { data, error, isLoading, refetch } = useBucket(id);
const { data: cors, isLoading: corsLoading } = useBucketCors(
data?.globalAliases[0]
);
const name = data?.globalAliases[0]; const name = data?.globalAliases[0];
@ -62,10 +65,15 @@ const ManageBucketPage = () => {
</Alert> </Alert>
)} )}
{data && ( {data && !corsLoading && (
<div className="container"> <div className="container">
<BucketContext.Provider <BucketContext.Provider
value={{ bucket: data, refetch, bucketName: name || "" }} value={{
bucket: data,
cors: cors ?? [],
refetch,
bucketName: name || "",
}}
> >
<TabView tabs={tabs} className="bg-base-100 h-14 px-1.5" /> <TabView tabs={tabs} className="bg-base-100 h-14 px-1.5" />
</BucketContext.Provider> </BucketContext.Provider>

View File

@ -0,0 +1,96 @@
import { useDenyKey } from "../hooks";
import { Card, Checkbox, Table } from "react-daisyui";
import Button from "@/components/ui/button";
import { Trash } from "lucide-react";
import AllowKeyDialog from "./allow-key-dialog";
import { useMemo } from "react";
import { toast } from "sonner";
import { handleError } from "@/lib/utils";
import { useBucketContext } from "../context";
const AccessKeyPermissions = () => {
const { bucket, refetch } = useBucketContext();
const denyKey = useDenyKey(bucket.id, {
onSuccess: () => {
toast.success("Key removed!");
refetch();
},
onError: handleError,
});
const keys = useMemo(() => {
return bucket?.keys.filter(
(key) =>
key.permissions.read !== false ||
key.permissions.write !== false ||
key.permissions.owner !== false
);
}, [bucket?.keys]);
const onRemove = (id: string) => {
if (window.confirm("Are you sure you want to remove this key?")) {
denyKey.mutate({
keyId: id,
permissions: { read: true, write: true, owner: true },
});
}
};
return (
<Card className="card-body">
<div className="flex flex-row items-center gap-2">
<Card.Title className="flex-1 truncate">Access Keys</Card.Title>
<AllowKeyDialog currentKeys={keys?.map((key) => key.accessKeyId)} />
</div>
<div className="overflow-x-auto">
<Table zebra size="sm">
<Table.Head>
<span>#</span>
<span>Key</span>
<span>Aliases</span>
<span>Read</span>
<span>Write</span>
<span>Owner</span>
<span />
</Table.Head>
<Table.Body>
{keys?.map((key, idx) => (
<Table.Row>
<span>{idx + 1}</span>
<span>{key.name || key.accessKeyId?.substring(0, 8)}</span>
<span>{key.bucketLocalAliases?.join(", ") || "-"}</span>
<span>
<Checkbox
checked={key.permissions?.read}
color="primary"
className="cursor-default"
/>
</span>
<span>
<Checkbox
checked={key.permissions?.write}
color="primary"
className="cursor-default"
/>
</span>
<span>
<Checkbox
checked={key.permissions?.owner}
color="primary"
className="cursor-default"
/>
</span>
<Button icon={Trash} onClick={() => onRemove(key.accessKeyId)} />
</Table.Row>
))}
</Table.Body>
</Table>
</div>
</Card>
);
};
export default AccessKeyPermissions;

View File

@ -19,7 +19,7 @@ type Props = {
}; };
const AllowKeyDialog = ({ currentKeys }: Props) => { const AllowKeyDialog = ({ currentKeys }: Props) => {
const { bucket } = useBucketContext(); const { bucket, bucketName } = useBucketContext();
const { dialogRef, isOpen, onOpen, onClose } = useDisclosure(); const { dialogRef, isOpen, onOpen, onClose } = useDisclosure();
const { data: keys } = useKeys(); const { data: keys } = useKeys();
const form = useForm<AllowKeysSchema>({ const form = useForm<AllowKeysSchema>({
@ -37,6 +37,7 @@ const AllowKeyDialog = ({ currentKeys }: Props) => {
onClose(); onClose();
toast.success("Key allowed!"); toast.success("Key allowed!");
queryClient.invalidateQueries({ queryKey: ["bucket", bucket.id] }); queryClient.invalidateQueries({ queryKey: ["bucket", bucket.id] });
queryClient.invalidateQueries({ queryKey: ["bucket_cors", bucketName] });
}, },
onError: handleError, onError: handleError,
}); });

View File

@ -0,0 +1,227 @@
import { Alert, Card, Modal } from "react-daisyui";
import Button from "@/components/ui/button";
import { CheckCircle, CircleXIcon, Plus } from "lucide-react";
import { useEffect, useRef } from "react";
import { toast } from "sonner";
import { useBucketContext } from "../context";
import { bucketCorsSchema, BucketCorsSchema } from "../schema";
import { zodResolver } from "@hookform/resolvers/zod";
import { Path, useForm, UseFormReturn } from "react-hook-form";
import Input, { InputField } from "@/components/ui/input";
import FormControl from "@/components/ui/form-control";
import { useDisclosure } from "@/hooks/useDisclosure";
import Chips from "@/components/ui/chips";
import { useBucketCorsMutation } from "../hooks";
import { handleError } from "@/lib/utils";
import { useQueryClient } from "@tanstack/react-query";
const CorsConfiguration = () => {
const queryClient = useQueryClient();
const { bucketName, cors, bucket } = useBucketContext();
const { mutate, isPending } = useBucketCorsMutation({
onSuccess: () => {
toast.success("CORS saved!");
queryClient.invalidateQueries({ queryKey: ["bucket_cors", bucketName] });
},
onError: handleError,
});
const form = useForm<BucketCorsSchema>({
resolver: zodResolver(bucketCorsSchema),
defaultValues: {
bucketName: bucketName,
rules: cors,
},
});
const fields = form.watch("rules");
function handleSubmit(data: BucketCorsSchema) {
mutate(data);
}
const allowConfiguration = bucket.keys.some((key) => key.permissions.owner);
return (
<Card className="card-body">
<form onSubmit={form.handleSubmit(handleSubmit)}>
<div className="flex flex-row items-center gap-2">
<Card.Title className="flex-1 truncate">
Cors Configuration
</Card.Title>
<Button
icon={CheckCircle}
color="primary"
type="submit"
disabled={isPending || !allowConfiguration}
>
Save
</Button>
</div>
{!allowConfiguration && (
<Alert status="warning" icon={<CircleXIcon />} className="mt-5">
<span>
You must configure an access key with owner permissions for this
bucket before setting up CORS.
</span>
</Alert>
)}
{allowConfiguration &&
fields.map((_, idx) => (
<div
className="grid md:grid-cols-2 xl:grid-cols-5 gap-5 mt-5"
key={idx}
>
<FormControl
form={form}
name={`rules.${idx}.allowedHeaders`}
title={"Allowed Headers"}
render={(field) => (
<CorsRulesChips
form={form}
fieldName={`rules.${idx}.allowedHeaders`}
values={(field.value as string[]) ?? []}
/>
)}
/>
<FormControl
form={form}
name={`rules.${idx}.allowedMethods`}
title={"Allowed Methods"}
render={(field) => (
<CorsRulesChips
form={form}
fieldName={`rules.${idx}.allowedMethods`}
values={(field.value as string[]) ?? []}
/>
)}
/>
<FormControl
form={form}
name={`rules.${idx}.allowedOrigins`}
title={"Allowed Origins"}
render={(field) => (
<CorsRulesChips
form={form}
fieldName={`rules.${idx}.allowedOrigins`}
values={(field.value as string[]) ?? []}
/>
)}
/>
<FormControl
form={form}
name={`rules.${idx}.exposeHeaders`}
title={"Expose Headers"}
render={(field) => (
<CorsRulesChips
form={form}
fieldName={`rules.${idx}.exposeHeaders`}
values={(field.value as string[]) ?? []}
/>
)}
/>
<InputField
form={form}
name={`rules.${idx}.maxAgeSeconds`}
title="Max age seconds"
placeholder="0000"
type="number"
/>
</div>
))}
</form>
</Card>
);
};
interface CorsRulesChipsProps {
form: UseFormReturn<BucketCorsSchema>;
fieldName: Path<BucketCorsSchema>;
values: string[];
}
function CorsRulesChips({ fieldName, form, values }: CorsRulesChipsProps) {
function onRemove(value: string) {
const currentValues =
(form.getValues(fieldName as Path<BucketCorsSchema>) as string[]) ?? [];
form.setValue(
fieldName,
currentValues.filter((v) => v !== value)
);
}
return (
<div className="flex flex-row flex-wrap gap-2 mt-2">
{values.map((value) => (
<Chips key={value} onRemove={() => onRemove(value)}>
{value}
</Chips>
))}
<AddRuleDialog form={form} fieldName={fieldName} />
</div>
);
}
interface AddRuleDialogProps {
form: UseFormReturn<BucketCorsSchema>;
fieldName: Path<BucketCorsSchema>;
}
const AddRuleDialog = ({ form, fieldName }: AddRuleDialogProps) => {
const { dialogRef, isOpen, onOpen, onClose } = useDisclosure();
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
inputRef.current.value = "";
}
}, [isOpen]);
function onSubmit() {
const value = inputRef.current?.value?.trim();
if (!value) {
onClose();
return;
}
const currentValues =
(form.getValues(fieldName as Path<BucketCorsSchema>) as string[]) ?? [];
form.setValue(
fieldName as Path<BucketCorsSchema>,
[...currentValues, value],
{ shouldDirty: true, shouldValidate: true }
);
onClose();
}
return (
<>
<Button type="button" size="sm" onClick={onOpen} icon={Plus}>
Add Rule
</Button>
<Modal ref={dialogRef} open={isOpen}>
<Modal.Header>Add Alias</Modal.Header>
<Modal.Body>
<Input ref={inputRef} className="w-full" />
</Modal.Body>
<Modal.Actions>
<Button type="button" onClick={onClose}>
Cancel
</Button>
<Button type="button" color="primary" onClick={onSubmit}>
Submit
</Button>
</Modal.Actions>
</Modal>
</>
);
};
export default CorsConfiguration;

View File

@ -1,99 +1,11 @@
import { useDenyKey } from "../hooks"; import AccessKeyPermissions from "./access-keys";
import { Card, Checkbox, Table } from "react-daisyui"; import CorsConfiguration from "./cors-configuration";
import Button from "@/components/ui/button";
import { Trash } from "lucide-react";
import AllowKeyDialog from "./allow-key-dialog";
import { useMemo } from "react";
import { toast } from "sonner";
import { handleError } from "@/lib/utils";
import { useBucketContext } from "../context";
const PermissionsTab = () => { const PermissionsTab = () => {
const { bucket, refetch } = useBucketContext();
const denyKey = useDenyKey(bucket.id, {
onSuccess: () => {
toast.success("Key removed!");
refetch();
},
onError: handleError,
});
const keys = useMemo(() => {
return bucket?.keys.filter(
(key) =>
key.permissions.read !== false ||
key.permissions.write !== false ||
key.permissions.owner !== false
);
}, [bucket?.keys]);
const onRemove = (id: string) => {
if (window.confirm("Are you sure you want to remove this key?")) {
denyKey.mutate({
keyId: id,
permissions: { read: true, write: true, owner: true },
});
}
};
return ( return (
<div> <div className="space-y-5">
<Card className="card-body"> <AccessKeyPermissions />
<div className="flex flex-row items-center gap-2"> <CorsConfiguration />
<Card.Title className="flex-1 truncate">Access Keys</Card.Title>
<AllowKeyDialog currentKeys={keys?.map((key) => key.accessKeyId)} />
</div>
<div className="overflow-x-auto">
<Table zebra size="sm">
<Table.Head>
<span>#</span>
<span>Key</span>
<span>Aliases</span>
<span>Read</span>
<span>Write</span>
<span>Owner</span>
<span />
</Table.Head>
<Table.Body>
{keys?.map((key, idx) => (
<Table.Row>
<span>{idx + 1}</span>
<span>{key.name || key.accessKeyId?.substring(0, 8)}</span>
<span>{key.bucketLocalAliases?.join(", ") || "-"}</span>
<span>
<Checkbox
checked={key.permissions?.read}
color="primary"
className="cursor-default"
/>
</span>
<span>
<Checkbox
checked={key.permissions?.write}
color="primary"
className="cursor-default"
/>
</span>
<span>
<Checkbox
checked={key.permissions?.owner}
color="primary"
className="cursor-default"
/>
</span>
<Button
icon={Trash}
onClick={() => onRemove(key.accessKeyId)}
/>
</Table.Row>
))}
</Table.Body>
</Table>
</div>
</Card>
</div> </div>
); );
}; };

View File

@ -37,3 +37,18 @@ export const allowKeysSchema = z.object({
}); });
export type AllowKeysSchema = z.infer<typeof allowKeysSchema>; export type AllowKeysSchema = z.infer<typeof allowKeysSchema>;
export const bucketCorsSchema = z.object({
bucketName: z.string(),
rules: z.array(
z.object({
allowedMethods: z.array(z.string()).nullable(),
allowedOrigins: z.array(z.string()).nullable(),
allowedHeaders: z.array(z.string()).nullable(),
exposeHeaders: z.array(z.string()).nullable(),
maxAgeSeconds: z.coerce.number().nullable(),
})
),
});
export type BucketCorsSchema = z.infer<typeof bucketCorsSchema>;

View File

@ -18,6 +18,14 @@ export type Bucket = {
quotas: Quotas; quotas: Quotas;
}; };
export type BucketCors = {
allowedMethods: string[] | null;
allowedOrigins: string[] | null;
allowedHeaders: string[] | null;
exposeHeaders: string[] | null;
maxAgeSeconds: number | null;
};
export type LocalAlias = { export type LocalAlias = {
accessKeyId: string; accessKeyId: string;
alias: string; alias: string;