Generate client-side secrets for admin auth
This commit is contained in:
parent
3d0a0e5111
commit
091468b776
@ -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();
|
||||
|
||||
32
src/components/CopyButton.tsx
Normal file
32
src/components/CopyButton.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
51
src/site/SecretGenerator.tsx
Normal file
51
src/site/SecretGenerator.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user