Skip to content

Commit

Permalink
Signing key rotation UI (#1297)
Browse files Browse the repository at this point in the history
  • Loading branch information
goodoldneon committed Apr 24, 2024
1 parent 1900c1d commit ae38834
Show file tree
Hide file tree
Showing 22 changed files with 862 additions and 92 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { Button } from '@inngest/components/Button';
import { RiAddLine } from '@remixicon/react';
import { useMutation } from 'urql';

import { graphql } from '@/gql';

const Mutation = graphql(`
mutation CreateSigningKey($envID: UUID!) {
createSigningKey(envID: $envID) {
createdAt
}
}
`);

type Props = {
disabled?: boolean;
envID: string;
};

export function CreateSigningKeyButton({ disabled, envID }: Props) {
const [, createSigningKey] = useMutation(Mutation);

return (
<Button
btnAction={() => createSigningKey({ envID })}
disabled={disabled}
kind="primary"
icon={<RiAddLine />}
label="Create new signing key"
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client';

import { useState } from 'react';
import { Button } from '@inngest/components/Button';
import { RiDeleteBin2Line } from '@remixicon/react';

import { DeleteSigningKeyModal } from './DeleteSigningKeyModal';

type Props = {
signingKeyID: string;
};

export function DeleteSigningKeyButton({ signingKeyID }: Props) {
const [isModalOpen, setIsModalOpen] = useState(false);

return (
<>
<Button
appearance="outlined"
aria-label="Delete"
btnAction={() => setIsModalOpen(true)}
icon={<RiDeleteBin2Line />}
kind="danger"
size="small"
tooltip="Delete"
/>

<DeleteSigningKeyModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
signingKeyID={signingKeyID}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useState } from 'react';
import { Alert } from '@inngest/components/Alert';
import { Button } from '@inngest/components/Button';
import { Modal } from '@inngest/components/Modal';
import { useMutation } from 'urql';

import { graphql } from '@/gql';

const Mutation = graphql(`
mutation DeleteSigningKey($signingKeyID: UUID!) {
deleteSigningKey(id: $signingKeyID) {
createdAt
}
}
`);

type Props = {
isOpen: boolean;
onClose: () => void;
signingKeyID: string;
};

export function DeleteSigningKeyModal(props: Props) {
const { isOpen, signingKeyID } = props;
const [error, setError] = useState<string>();
const [isFetching, setIsFetching] = useState(false);
const [, deleteSigningKey] = useMutation(Mutation);

function onClose() {
setError(undefined);
props.onClose();
}

async function onConfirm() {
setIsFetching(true);
try {
const res = await deleteSigningKey(
{ signingKeyID },
{
// Bust cache
additionalTypenames: ['SigningKey'],
}
);
if (res.error) {
throw res.error;
}

onClose();
} catch (error) {
if (!(error instanceof Error)) {
setError('Unknown error');
return;
}

setError(error.message);
} finally {
setIsFetching(false);
}
}

return (
<Modal className="w-full max-w-3xl" isOpen={isOpen} onClose={onClose}>
<Modal.Header>Permanently delete key</Modal.Header>

<Modal.Body>
<p className="mb-4">Are you sure you want to permanently delete this key?</p>

<Alert severity="info">
This key is inactive, so deletion will not affect communication between Inngest and your
apps.
</Alert>
</Modal.Body>

<Modal.Footer>
{error && (
<Alert className="mb-6" severity="error">
{error}
</Alert>
)}

<div className="flex justify-end gap-2">
<Button label="Close" appearance="outlined" btnAction={onClose} />
<Button
btnAction={onConfirm}
disabled={isFetching}
kind="danger"
label="Permanently delete"
/>
</div>
</Modal.Footer>
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DeleteSigningKeyButton } from './DeleteSigningKeyButton';
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import cn from '@/utils/cn';

type Props = {
className?: string;
value: string;
};

export function InlineCode({ value }: Props) {
export function InlineCode({ className, value }: Props) {
return (
<code className="inline-flex items-center rounded bg-slate-200 px-2 py-1 font-mono text-xs font-semibold leading-none">
<code
className={cn(
'inline-flex items-center rounded-sm bg-slate-200 px-1 py-1 font-mono text-xs font-semibold leading-none',
className
)}
>
{value}
</code>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import { useState } from 'react';
import { Button } from '@inngest/components/Button';

import { RotateSigningKeyModal } from './RotateSigningKeyModal';

type Props = {
disabled?: boolean;
envID: string;
};

export function RotateSigningKeyButton({ disabled, envID }: Props) {
const [isModalOpen, setIsModalOpen] = useState(false);

return (
<>
<Button
btnAction={() => setIsModalOpen(true)}
disabled={disabled}
kind="danger"
label="Rotate key"
/>

<RotateSigningKeyModal
envID={envID}
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useState } from 'react';
import { Alert } from '@inngest/components/Alert';
import { Button } from '@inngest/components/Button';
import { Modal } from '@inngest/components/Modal';
import { useMutation } from 'urql';

import { graphql } from '@/gql';
import { InlineCode } from '../InlineCode';

const Mutation = graphql(`
mutation RotateSigningKey($envID: UUID!) {
rotateSigningKey(envID: $envID) {
createdAt
}
}
`);

type Props = {
envID: string;
isOpen: boolean;
onClose: () => void;
};

export function RotateSigningKeyModal(props: Props) {
const { envID, isOpen } = props;
const [error, setError] = useState<string>();
const [isFetching, setIsFetching] = useState(false);
const [, rotateSigningKey] = useMutation(Mutation);

function onClose() {
setError(undefined);
props.onClose();
}

async function onConfirm() {
setIsFetching(true);
try {
const res = await rotateSigningKey(
{ envID },
{
// Bust cache
additionalTypenames: ['SigningKey'],
}
);
if (res.error) {
throw res.error;
}

setError(undefined);
onClose();
} catch (error) {
if (!(error instanceof Error)) {
setError('Unknown error');
return;
}

setError(error.message);
} finally {
setIsFetching(false);
}
}

return (
<Modal className="w-full max-w-3xl" isOpen={isOpen} onClose={onClose}>
<Modal.Header>Rotate key</Modal.Header>

<Modal.Body>
<p className="mb-4">
Before rotating, ensure that all of your apps have the correct{' '}
<InlineCode value="INNGEST_SIGNING_KEY" /> and{' '}
<InlineCode value="INNGEST_SIGNING_KEY_FALLBACK" /> environment variables.
</p>

<Alert severity="warning">
This will permanently delete and replace the current key. It is irreversible.
</Alert>
</Modal.Body>

<Modal.Footer>
{error && (
<Alert className="mb-6" severity="error">
{error}
</Alert>
)}

<div className="flex justify-end gap-2">
<Button label="Close" appearance="outlined" btnAction={onClose} />
<Button btnAction={onConfirm} disabled={isFetching} kind="danger" label="Rotate" />
</div>
</Modal.Footer>
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RotateSigningKeyButton } from './RotateSigningKeyButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Badge } from '@inngest/components/Badge';
import { Card } from '@inngest/components/Card';
import { RiStarFill } from '@remixicon/react';

import { Secret } from '@/components/Secret';
import { Time } from '@/components/Time';
import { DeleteSigningKeyButton } from './DeleteSigningKeyButton';

type Props = {
signingKey: {
createdAt: Date;
decryptedValue: string;
id: string;
isActive: boolean;
user: {
email: string;
name: string | null;
} | null;
};
};

export function SigningKey({ signingKey }: Props) {
let accentColor = 'bg-emerald-600';
let controls = null;
let description = null;
let title = 'Current key';

if (!signingKey.isActive) {
accentColor = 'bg-amber-400';
controls = (
<div className="flex gap-2">
<DeleteSigningKeyButton signingKeyID={signingKey.id} />
</div>
);
description = 'This key is inactive. You can activate it using rotation.';
title = 'New key';
}

let pill = null;
if (signingKey.createdAt > new Date(Date.now() - 24 * 60 * 60 * 1000)) {
pill = (
<Badge className="border-0 bg-amber-100">
<RiStarFill className="text-amber-600" size={16} />
<span className="text-amber-600">New</span>
</Badge>
);
}

return (
<Card accentColor={accentColor} accentPosition="left" className="mb-4">
<Card.Content className="px-4 py-0">
<div className="py-4">
<div className="flex">
<div className="flex grow items-center gap-2 font-medium text-slate-950">
{title}
{pill}
</div>
{controls && <div>{controls}</div>}
</div>
<p className="text-sm text-slate-500">{description}</p>
</div>

<Secret className="mb-4" kind="signing-key" secret={signingKey.decryptedValue} />
</Card.Content>

<Card.Footer className="flex text-sm text-slate-500">
<span className="grow">
Created at <Time value={signingKey.createdAt} />
</span>

{signingKey.user && <span>Created by {signingKey.user.name ?? signingKey.user.email}</span>}
</Card.Footer>
</Card>
);
}
Loading

0 comments on commit ae38834

Please sign in to comment.