EAM: Add AWS credentials validation (#821)
* Add AWS credentials validation * Add changelog
This commit is contained in:
parent
54901f8d08
commit
43d7e47bd4
4 changed files with 105 additions and 7 deletions
5
.changeset/mean-ghosts-beam.md
Normal file
5
.changeset/mean-ghosts-beam.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-products-feed": patch
|
||||
---
|
||||
|
||||
Added validation for AWS credentials. If provided configuration for S3 Bucket is invalid, it won't be saved.
|
|
@ -5,6 +5,9 @@ import { createLogger } from "@saleor/apps-shared";
|
|||
import { updateCacheForConfigurations } from "../metadata-cache/update-cache-for-configurations";
|
||||
import { AppConfigSchema } from "./app-config";
|
||||
import { z } from "zod";
|
||||
import { createS3ClientFromConfiguration } from "../file-storage/s3/create-s3-client-from-configuration";
|
||||
import { checkBucketAccess } from "../file-storage/s3/check-bucket-access";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export const appConfigurationRouter = router({
|
||||
/**
|
||||
|
@ -17,13 +20,54 @@ export const appConfigurationRouter = router({
|
|||
return c.getRootConfig();
|
||||
});
|
||||
}),
|
||||
testS3BucketConfiguration: protectedClientProcedure
|
||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||
.input(AppConfigSchema.s3Bucket)
|
||||
.mutation(async ({ ctx: { saleorApiUrl, getConfig, appConfigMetadataManager }, input }) => {
|
||||
const logger = createLogger({ saleorApiUrl: saleorApiUrl });
|
||||
|
||||
logger.debug("Validate the credentials");
|
||||
|
||||
const s3Client = createS3ClientFromConfiguration(input);
|
||||
|
||||
try {
|
||||
await checkBucketAccess({
|
||||
bucketName: input.bucketName,
|
||||
s3Client,
|
||||
});
|
||||
} catch {
|
||||
logger.debug("Validation failed");
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Could not access the S3 bucket using the provided credentials",
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
setS3BucketConfiguration: protectedClientProcedure
|
||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||
.input(AppConfigSchema.s3Bucket)
|
||||
.mutation(async ({ ctx: { saleorApiUrl, getConfig, appConfigMetadataManager }, input }) => {
|
||||
const logger = createLogger({ saleorApiUrl: saleorApiUrl });
|
||||
|
||||
logger.debug(input, "Input");
|
||||
logger.debug("Validate credentials");
|
||||
|
||||
const s3Client = createS3ClientFromConfiguration(input);
|
||||
|
||||
try {
|
||||
await checkBucketAccess({
|
||||
bucketName: input.bucketName,
|
||||
s3Client,
|
||||
});
|
||||
} catch {
|
||||
logger.debug("Validation failed");
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Could not access the S3 bucket using the provided credentials",
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("Credentials validated, saving");
|
||||
|
||||
const config = await getConfig();
|
||||
|
||||
|
|
|
@ -14,10 +14,11 @@ type S3BucketConfiguration = Exclude<RootConfig["s3"], null>;
|
|||
type Props = {
|
||||
initialData: S3BucketConfiguration;
|
||||
onSubmit(data: S3BucketConfiguration): Promise<void>;
|
||||
onValidate(data: S3BucketConfiguration): Promise<void>;
|
||||
};
|
||||
|
||||
export const S3ConfigurationForm = (props: Props) => {
|
||||
const { handleSubmit, control } = useForm<S3BucketConfiguration>({
|
||||
const { handleSubmit, control, getValues } = useForm<S3BucketConfiguration>({
|
||||
defaultValues: props.initialData,
|
||||
resolver: zodResolver(AppConfigSchema.s3Bucket),
|
||||
});
|
||||
|
@ -53,10 +54,15 @@ export const S3ConfigurationForm = (props: Props) => {
|
|||
placeholder={"eu-west-1"}
|
||||
/>
|
||||
|
||||
<Button type="submit" variant="primary" alignSelf={"end"}>
|
||||
<Box display={"flex"} flexDirection={"row"} gap={4} justifyContent={"flex-end"}>
|
||||
<Button variant="secondary" onClick={() => props.onValidate(getValues())}>
|
||||
Test credentials
|
||||
</Button>
|
||||
<Button type="submit" variant="primary">
|
||||
Save bucket configuration
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -67,10 +73,25 @@ export const ConnectedS3ConfigurationForm = () => {
|
|||
onSuccess() {
|
||||
notifySuccess("Success", "Updated S3 configuration");
|
||||
},
|
||||
onError() {
|
||||
onError({ message }) {
|
||||
if (message) {
|
||||
notifyError("Error", message);
|
||||
return;
|
||||
}
|
||||
notifyError("Error", "Failed to update, please refresh and try again");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: testConfigurationMutate } =
|
||||
trpcClient.appConfiguration.testS3BucketConfiguration.useMutation({
|
||||
onSuccess() {
|
||||
notifySuccess("Configuration is valid");
|
||||
},
|
||||
onError({ message }) {
|
||||
notifyError("Error", message);
|
||||
},
|
||||
});
|
||||
|
||||
const { data, isLoading } = trpcClient.appConfiguration.fetch.useQuery();
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
|
@ -80,6 +101,13 @@ export const ConnectedS3ConfigurationForm = () => {
|
|||
[mutate]
|
||||
);
|
||||
|
||||
const handleValidate = useCallback(
|
||||
async (data: S3BucketConfiguration) => {
|
||||
testConfigurationMutate(data);
|
||||
},
|
||||
[testConfigurationMutate]
|
||||
);
|
||||
|
||||
const formData: S3BucketConfiguration = useMemo(() => {
|
||||
if (data?.s3) {
|
||||
return data.s3;
|
||||
|
@ -97,5 +125,11 @@ export const ConnectedS3ConfigurationForm = () => {
|
|||
return <Text>Loading...</Text>;
|
||||
}
|
||||
|
||||
return <S3ConfigurationForm onSubmit={handleSubmit} initialData={formData} />;
|
||||
return (
|
||||
<S3ConfigurationForm
|
||||
onSubmit={handleSubmit}
|
||||
initialData={formData}
|
||||
onValidate={handleValidate}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { HeadBucketCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
interface checkBucketAccessArgs {
|
||||
s3Client: S3Client;
|
||||
bucketName: string;
|
||||
}
|
||||
|
||||
// Check if client can access the bucket. Throws an error otherwise
|
||||
export const checkBucketAccess = async ({ s3Client, bucketName }: checkBucketAccessArgs) => {
|
||||
await s3Client.send(
|
||||
new HeadBucketCommand({
|
||||
Bucket: bucketName,
|
||||
})
|
||||
);
|
||||
};
|
Loading…
Reference in a new issue