Generate client-side secrets for admin auth

This commit is contained in:
Sam Becker 2025-01-23 21:41:35 -06:00
parent 3d0a0e5111
commit 091468b776
5 changed files with 91 additions and 41 deletions

View File

@ -6,6 +6,7 @@ import {
KEY_CREDENTIALS_SIGN_IN_ERROR,
KEY_CREDENTIALS_SIGN_IN_ERROR_URL,
auth,
generateAuthSecret,
signIn,
signOut,
} from '@/auth';
@ -47,3 +48,5 @@ export const getAuthAction = async () => auth();
export const logClientAuthUpdate = async (data: Session | null | undefined) =>
console.log('Client auth update', data);
export const generateAuthSecretAction = async () => generateAuthSecret();

View File

@ -0,0 +1,32 @@
import { BiCopy } from 'react-icons/bi';
import LoaderButton from './primitives/LoaderButton';
import clsx from 'clsx/lite';
import { toastSuccess } from '@/toast';
export default function CopyButton({
label,
text,
subtle,
}: {
label: string
text?: string,
subtle?: boolean
}) {
return (
<LoaderButton
icon={<BiCopy size={15} />}
className={clsx(
'translate-y-[2px]',
subtle && 'text-gray-300 dark:text-gray-700',
)}
onClick={text
? () => {
navigator.clipboard.writeText(text);
toastSuccess(`${label} copied to clipboard`);
}
: undefined}
styleAs="link"
disabled={!text}
/>
);
}

View File

@ -0,0 +1,51 @@
'use client';
import { clsx } from 'clsx/lite';
import Container from '@/components/Container';
import Spinner from '@/components/Spinner';
import CopyButton from '@/components/CopyButton';
import { useCallback, useEffect, useState } from 'react';
import { generateAuthSecretAction } from '@/auth/actions';
import { BiRefresh } from 'react-icons/bi';
export default function SecretGenerator() {
const [isLoading, setIsLoading] = useState(false);
const [secret, setSecret] = useState('');
const getSecret = useCallback(async () => {
setIsLoading(true);
await generateAuthSecretAction()
.then(setSecret)
.finally(() => setIsLoading(false));
}, []);
useEffect(() => {
getSecret();
}, [getSecret]);
return (
<div className="flex items-center gap-2">
<Container className="my-1.5 inline-flex" padding="tight">
<div className={clsx(
'flex flex-nowrap items-center gap-2 leading-none -mx-1',
)}>
{secret ? <span>{secret}</span> : <Spinner />}
<div
className="flex items-center gap-0.5 translate-y-[-2px]"
>
<CopyButton label="Secret" text={secret} />
</div>
</div>
</Container>
{secret && <div className="flex items-center justify-center w-6">
{isLoading
? <Spinner />
: <BiRefresh
className="cursor-pointer active:translate-y-[1px] shrink-0"
onClick={getSecret}
size={18}
/>}
</div>}
</div>
);
}

View File

@ -9,26 +9,23 @@ import ChecklistRow from '../components/ChecklistRow';
import { FiExternalLink } from 'react-icons/fi';
import {
BiCog,
BiCopy,
BiData,
BiHide,
BiLockAlt,
BiPencil,
} from 'react-icons/bi';
import Container from '@/components/Container';
import Checklist from '@/components/Checklist';
import { toastSuccess } from '@/toast';
import { ConfigChecklistStatus } from './config';
import StatusIcon from '@/components/StatusIcon';
import { labelForStorage } from '@/services/storage';
import { HiSparkles } from 'react-icons/hi';
import LoaderButton from '@/components/primitives/LoaderButton';
import { testConnectionsAction } from '@/admin/actions';
import ErrorNote from '@/components/ErrorNote';
import Spinner from '@/components/Spinner';
import WarningNote from '@/components/WarningNote';
import { RiSpeedMiniLine } from 'react-icons/ri';
import Link from 'next/link';
import SecretGenerator from './SecretGenerator';
import CopyButton from '@/components/CopyButton';
export default function SiteChecklistClient({
// Storage
@ -94,12 +91,10 @@ export default function SiteChecklistClient({
// Component props
simplifiedView,
isTestingConnections,
secret,
}: ConfigChecklistStatus &
Partial<Awaited<ReturnType<typeof testConnectionsAction>>> & {
simplifiedView?: boolean
isTestingConnections?: boolean
secret?: string
}) {
const renderLink = (href: string, text: string, external = true) =>
<>
@ -122,23 +117,6 @@ export default function SiteChecklistClient({
</>}
</>;
const renderCopyButton = (label: string, text?: string, subtle?: boolean) =>
<LoaderButton
icon={<BiCopy size={15} />}
className={clsx(
'translate-y-[2px]',
subtle && 'text-gray-300 dark:text-gray-700',
)}
onClick={text
? () => {
navigator.clipboard.writeText(text);
toastSuccess(`${label} copied to clipboard`);
}
: undefined}
styleAs="link"
disabled={!text}
/>;
const renderEnvVar = (
variable: string,
minimal?: boolean,
@ -159,7 +137,7 @@ export default function SiteChecklistClient({
)}>
`{variable}`
</span>
{!minimal && renderCopyButton(variable, variable, true)}
{!minimal && <CopyButton label={variable} text={variable} subtle />}
</span>
</div>;
@ -321,20 +299,9 @@ export default function SiteChecklistClient({
isPending={!hasAuthSecret && isTestingConnections}
>
Store auth secret in environment variable:
{!hasAuthSecret || true &&
{!hasAuthSecret &&
<div className="overflow-x-auto">
<Container className="my-1.5 inline-flex" padding="tight">
<div className={clsx(
'flex flex-nowrap items-center gap-2 leading-none -mx-1',
)}>
{secret ? <span>{secret}</span> : <Spinner />}
<div
className="flex items-center gap-0.5 translate-y-[-2px]"
>
{renderCopyButton('Secret', secret)}
</div>
</div>
</Container>
<SecretGenerator />
</div>}
{renderEnvVars(['AUTH_SECRET'])}
</ChecklistRow>

View File

@ -1,4 +1,3 @@
import { generateAuthSecret } from '@/auth';
import SiteChecklistClient from './SiteChecklistClient';
import { CONFIG_CHECKLIST_STATUS } from '@/site/config';
import { testConnectionsAction } from '@/admin/actions';
@ -8,14 +7,12 @@ export default async function SiteChecklistServer({
}: {
simplifiedView?: boolean
}) {
const secret = await generateAuthSecret().catch(() => 'TRY AGAIN');
const connectionErrors = await testConnectionsAction().catch(() => ({}));
return (
<SiteChecklistClient {...{
...CONFIG_CHECKLIST_STATUS,
...connectionErrors,
simplifiedView,
secret,
}} />
);
}