feat(pam): PAM Platform V1

This commit is contained in:
x032205
2025-09-27 01:57:43 -04:00
parent 270d8237fe
commit 4b1664c30f
141 changed files with 9327 additions and 46 deletions
@@ -0,0 +1,49 @@
import { Controller, useFormContext } from "react-hook-form";
import { z } from "zod";
import { FormControl, Input, TextArea } from "@app/components/v2";
import { slugSchema } from "@app/lib/schemas";
export const genericAccountFieldsSchema = z.object({
name: slugSchema({ min: 1, max: 64, field: "Name" }),
description: z.string().max(512).nullable().optional()
});
export const GenericAccountFields = () => {
const {
formState: { errors },
control
} = useFormContext<{ name: string; description: string }>();
return (
<>
<Controller
name="name"
control={control}
render={({ field }) => (
<FormControl
helperText="Name must be slug-friendly"
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Name"
>
<Input autoFocus placeholder="my-account" {...field} />
</FormControl>
)}
/>
<Controller
name="description"
control={control}
render={({ field }) => (
<FormControl
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Description"
>
<TextArea {...field} />
</FormControl>
)}
/>
</>
);
};
@@ -0,0 +1,147 @@
import { createNotification } from "@app/components/notifications";
import {
PamResourceType,
TPamAccount,
useCreatePamAccount,
useUpdatePamAccount
} from "@app/hooks/api/pam";
import { DiscriminativePick } from "@app/types";
import { PamAccountHeader } from "../PamAccountHeader";
import { PostgresAccountForm } from "./PostgresAccountForm";
type FormProps = {
onComplete: (account: TPamAccount) => void;
};
type CreateFormProps = FormProps & {
projectId: string;
resourceId: string;
resourceType: PamResourceType;
folderId?: string;
};
type UpdateFormProps = FormProps & {
account: TPamAccount;
};
const CreateForm = ({
onComplete,
projectId,
resourceId,
resourceType,
folderId
}: CreateFormProps) => {
const createPamAccount = useCreatePamAccount();
console.log({ folderId });
const onSubmit = async (
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
) => {
try {
const account = await createPamAccount.mutateAsync({
...formData,
folderId,
resourceId,
resourceType,
projectId
});
createNotification({
text: "Successfully created account",
type: "success"
});
onComplete(account);
} catch (err: any) {
console.error(err);
createNotification({
title: "Failed to create account",
text: err.message,
type: "error"
});
}
};
switch (resourceType) {
case PamResourceType.Postgres:
return <PostgresAccountForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled resource: ${resourceType}`);
}
};
const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
const updatePamAccount = useUpdatePamAccount();
const onSubmit = async (
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
) => {
try {
const updatedAccount = await updatePamAccount.mutateAsync({
accountId: account.id,
resourceId: account.resourceId,
resourceType: account.resource.resourceType,
...formData
});
createNotification({
text: "Successfully updated account",
type: "success"
});
onComplete(updatedAccount);
} catch (err: any) {
console.error(err);
createNotification({
title: "Failed to update account",
text: err.message,
type: "error"
});
}
};
switch (account.resource.resourceType) {
case PamResourceType.Postgres:
return <PostgresAccountForm account={account} onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
}
};
type Props = {
onBack?: () => void;
projectId: string;
} & FormProps &
(
| {
account: TPamAccount;
resourceId?: undefined;
resourceName?: undefined;
resourceType?: undefined;
folderId?: undefined;
}
| {
account?: undefined;
resourceId: string;
resourceName: string;
resourceType: PamResourceType;
folderId?: string;
}
);
export const PamAccountForm = ({ onBack, projectId, ...props }: Props) => {
const { account, resourceName, resourceType } = props;
return (
<div>
<PamAccountHeader
resourceName={account ? account.resource.name : resourceName}
resourceType={account ? account.resource.resourceType : resourceType}
onBack={onBack}
/>
{account ? (
<UpdateForm {...props} account={account} />
) : (
<CreateForm {...props} projectId={projectId} />
)}
</div>
);
};
@@ -0,0 +1,73 @@
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, ModalClose } from "@app/components/v2";
import { TPostgresAccount } from "@app/hooks/api/pam";
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
import { SqlAccountFields } from "./shared/SqlAccountFields";
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
type Props = {
account?: TPostgresAccount;
onSubmit: (formData: FormData) => Promise<void>;
};
const formSchema = genericAccountFieldsSchema.extend({
credentials: BaseSqlAccountSchema
});
type FormData = z.infer<typeof formSchema>;
export const PostgresAccountForm = ({ account, onSubmit }: Props) => {
const isUpdate = Boolean(account);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: account
? {
...account,
credentials: {
...account.credentials,
password: "******"
}
}
: undefined
});
const {
handleSubmit,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
handleSubmit(onSubmit)(e);
}}
>
<GenericAccountFields />
<SqlAccountFields isUpdate={isUpdate} />
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Account" : "Create Account"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};
@@ -0,0 +1,55 @@
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, Input } from "@app/components/v2";
export const SqlAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
const { control } = useFormContext();
return (
<div className="flex gap-2">
<Controller
name="credentials.username"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Username"
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
name="credentials.password"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
className="flex-1"
errorText={error?.message}
isError={Boolean(error?.message)}
label="Password"
>
<Input
{...field}
type="password"
onFocus={(e) => {
if (isUpdate && field.value === "******") {
field.onChange("");
}
e.target.type = "text";
}}
onBlur={(e) => {
if (isUpdate && field.value === "") {
field.onChange("******");
}
e.target.type = "password";
}}
/>
</FormControl>
)}
/>
</div>
);
};
@@ -0,0 +1,6 @@
import { z } from "zod";
export const BaseSqlAccountSchema = z.object({
username: z.string().trim().min(1, "Username required"),
password: z.string().trim().min(1, "Password required")
});