Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip - api key dialog. #4448

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "@i18n/client";
import { Button } from "@clientComponents/globals";
import { Dialog, useDialogRef } from "@formBuilder/components/shared/Dialog";
import {
APIKeyCustomEventDetails,
CustomEventDetails,
EventKeys,
useCustomEvent,
} from "@lib/hooks/useCustomEvent";

import { Trans } from "react-i18next";

import { cn } from "@lib/utils";

export const TextInput = ({
label,
required,
description,
children,
}: {
label: string;
required: React.ReactElement | null;
description: string;
children: React.ReactElement;
}) => {
return (
<div className="mb-4">
<label className="mb-2 block font-bold">
{label} {required && required}
</label>
<p className="mb-4">
<Trans
ns="form-builder"
i18nKey={description}
defaults="<strong></strong> <a></a>"
components={{ strong: <strong />, a: <a /> }}
/>
</p>
{children}
</div>
);
};

const ConfirmationAgreement = ({
handleAgreement,
}: {
handleAgreement: (value: string) => void;
}) => {
const { t } = useTranslation("form-builder");
const [agreeValue, setAgreeValue] = useState("");

return (
<div className="mb-4 ">
<TextInput
label={t("settings.api.dialog.confirm.label")}
required={
<span className="text-gcds-red-500">{t("settings.api.dialog.confirm.required")}</span>
}
description="settings.api.dialog.confirm.description"
>
<div>
<input
type="text"
className="gc-input-text mb-1 w-4/5"
value={agreeValue}
onChange={(e) => {
setAgreeValue(e.target.value);
handleAgreement(e.target.value);
}}
/>
</div>
</TextInput>
</div>
);
};

export const ApiKeyDialog = () => {
const dialog = useDialogRef();
const { Event } = useCustomEvent();
const { t } = useTranslation("form-builder");
const [handler, setHandler] = useState<APIKeyCustomEventDetails | null>(null);

const [isOpen, setIsOpen] = useState(false);

const handleClose = () => {
dialog.current?.close();
handler?.cancel();
setIsOpen(false);
};

const handleOpenDialog = useCallback((detail: CustomEventDetails) => {
if (detail) {
setHandler(detail);
setIsOpen(true);
}
}, []);

const handleSave = () => {
handler?.download();
handleClose();
};

useEffect(() => {
Event.on(EventKeys.openApiKeyDialog, handleOpenDialog);
return () => {
Event.off(EventKeys.openApiKeyDialog, handleOpenDialog);
};
}, [Event, handleOpenDialog]);

const [agreed, setAgreed] = useState(false);

const hasAgreed = (value: string) => {
if (value === "AGREE") {
setAgreed(true);
} else {
setAgreed(false);
}
};

const actions = (
<>
<Button
theme="secondary"
onClick={() => {
dialog.current?.close();
handleClose && handleClose();
}}
>
{t("settings.api.dialog.cancelButton")}
</Button>
<Button
className={cn("ml-5")}
theme="primary"
disabled={!agreed}
onClick={handleSave}
dataTestId="confirm-download"
>
{t("settings.api.dialog.downloadButton")}
</Button>
</>
);

return (
<>
{isOpen && (
<Dialog
handleClose={handleClose}
dialogRef={dialog}
actions={actions}
title={t("settings.api.dialog.title")}
>
<div className="p-5">
<h4 className="mb-4">{t("settings.api.dialog.heading")}</h4>
<p className="font-bold">{t("settings.api.dialog.responsibility")}</p>
<ul className="mb-4">
<li>
<Trans
ns="form-builder"
i18nKey="settings.api.dialog.responsibility1"
defaults="<strong></strong> <a></a>"
components={{ strong: <strong />, a: <a /> }}
/>
</li>
<li>
<Trans
ns="form-builder"
i18nKey="settings.api.dialog.responsibility2"
defaults="<strong></strong> <a></a>"
components={{ strong: <strong />, a: <a /> }}
/>
</li>
<li>
<Trans
ns="form-builder"
i18nKey="settings.api.dialog.responsibility3"
defaults="<strong></strong> <a></a>"
components={{ strong: <strong />, a: <a /> }}
/>
</li>
<li>
<Trans
ns="form-builder"
i18nKey="settings.api.dialog.responsibility4"
defaults="<strong></strong> <a></a>"
components={{ strong: <strong />, a: <a /> }}
/>
</li>
</ul>

<ConfirmationAgreement handleAgreement={hasAgreed} />

<p className="font-bold">{t("settings.api.dialog.text1")}</p>
<p>{t("settings.api.dialog.text2")}</p>
</div>
</Dialog>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
"use client";
import { Button } from "@clientComponents/globals";
import { useTranslation } from "@i18n/client";
import { CircleCheckIcon } from "@serverComponents/icons";
import {
createServiceAccountKey,
refreshServiceAccountKey,
deleteServiceAccountKey,
} from "../../actions";
import { useParams } from "next/navigation";
import { useState } from "react";
import { EventKeys, useCustomEvent } from "@lib/hooks/useCustomEvent";
import { ApiKeyType } from "@lib/types/form-builder-types";
import { Tooltip } from "@formBuilder/components/shared/Tooltip";

const _createKey = async (templateId: string) => {
// In the future this could be done in the browser but we'll need to verify that they key meets the requirements
const key = await createServiceAccountKey(templateId);
downloadKey(JSON.stringify(key), templateId);
return key;
};

// For future use
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _refreshKey = async (templateId: string) => {
const key = await refreshServiceAccountKey(templateId);
downloadKey(JSON.stringify(key), templateId);
return key;
};

const downloadKey = (key: string, templateId: string) => {
Expand All @@ -36,31 +40,71 @@ const downloadKey = (key: string, templateId: string) => {
URL.revokeObjectURL(href);
};

export const ApiKey = ({ keyExists }: { keyExists?: boolean }) => {
const ApiTooltip = () => {
const { t } = useTranslation("form-builder");

return (
<Tooltip.Info
side="top"
triggerClassName="align-middle ml-1"
tooltipClassName="font-normal whitespace-normal"
>
<strong>{t("settings.api.keyIdToolTip.text1")}</strong>
<p>{t("settings.api.keyIdToolTip.text2")}</p>
</Tooltip.Info>
);
};

export const ApiKey = ({ keyId }: { keyId?: string | false }) => {
const { t } = useTranslation("form-builder");
const { id } = useParams();
const { Event } = useCustomEvent();
const [key, setKey] = useState<ApiKeyType | null>(null);
if (Array.isArray(id)) return null;

const openDialog = () => {
Event.fire(EventKeys.openApiKeyDialog, {
download: () => {
downloadKey(JSON.stringify(key), id);
setKey(null);
},
cancel: () => {
setKey(null);
deleteServiceAccountKey(id);
},
});
};

return (
<div className="mb-10">
<div className="mb-4">
<h2 className="mb-6">{t("settings.api.title")}</h2>
</div>
<div className="mb-4">
{keyExists ? (
{keyId && !key ? (
<>
<div className="mb-4">
<CircleCheckIcon className="mr-2 inline-block w-9 fill-green-700" />
{t("settings.api.keyExists")}
<div className="font-bold">{t("settings.api.keyId")}</div>
{keyId} <ApiTooltip />
</div>
<Button theme="primary" className="mr-4" onClick={() => deleteServiceAccountKey(id)}>

<Button
theme="destructive"
className="mr-4"
onClick={() => deleteServiceAccountKey(id)}
>
{t("settings.api.deleteKey")}
</Button>
<Button theme="primary" onClick={() => _refreshKey(id)}>
{t("settings.api.refreshKey")}
</Button>
</>
) : (
<Button theme="primary" onClick={() => _createKey(id)}>
<Button
theme="primary"
onClick={async () => {
const key = await _createKey(id);
setKey(key);
openDialog();
}}
>
{t("settings.api.generateKey")}
</Button>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { checkKeyExists } from "@lib/serviceAccount";
import { redirect } from "next/navigation";
import { checkPrivilegesAsBoolean } from "@lib/privileges";
import { isProductionEnvironment } from "@lib/origin";
import { ApiKeyDialog } from "../../components/dialogs/APIKeyDialog";

export async function generateMetadata({
params: { locale },
Expand Down Expand Up @@ -38,7 +39,12 @@ export default async function Page({
redirect(`/${locale}/form-builder/${id}/settings`);
}

const keyExists = await checkKeyExists(id);
const keyId = await checkKeyExists(id);

return <ApiKey keyExists={keyExists} />;
return (
<>
<ApiKey keyId={keyId} />
<ApiKeyDialog />
</>
);
}
29 changes: 26 additions & 3 deletions i18n/translations/en/form-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,34 @@
"settings": {
"formManagement": "Form management",
"api": {
"keyId": "API Key ID",
"title": "API Access",
"generateKey": "Generate Key",
"generateKey": "Generate API Key",
"refreshKey": "Refresh Key",
"deleteKey": "Delete Key",
"keyExists": "API Key Exists"
"deleteKey": "Delete API Key",
"keyExists": "API Key Exists",
"keyIdToolTip": {
"text1": "What is an API key ID?",
"text2": "The API key ID is not the API key itself. It provides a safe way to identify a key without sharing protected information."
},
"dialog": {
"title": "API key successfully created",
"heading": "GC Forms API keys are PROTECTED B.",
"responsibility": "It is your responsibility to:",
"responsibility1": "Save this key in a secure location.",
"responsibility2": "Share the key only with people who need it.",
"responsibility3": "Send the key through secure channels, like encrypted email.",
"responsibility4": "Read and the understand the <a href='canada.ca' target='_blank'>required security procedures.</a>",
"text1": "Do not loose your key",
"text2": "This is your unique password and it is non-recoverable. If you loose this key, you will have to create a new one.",
"cancelButton": "Cancel",
"downloadButton": "Download key",
"confirm": {
"label": "Confirmation",
"required": "(required)",
"description": "Type <strong>AGREE</strong> in the box below to confirm you understand your responsibilities and agree to the <a href='canada.ca' target='_blank'>required security procedures</a> while storing and sharing this key."
}
}
}
},
"settingsDeleteButton": "Delete",
Expand Down
Loading
Loading