mirror of
https://github.com/documenso/documenso.git
synced 2025-11-17 10:11:35 +10:00
fix: update teams API tokens logic
This commit is contained in:
@ -30,22 +30,20 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type TokenDeleteDialogProps = {
|
||||
teamId?: number;
|
||||
token: Pick<ApiToken, 'id' | 'name'>;
|
||||
onDelete?: () => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function TokenDeleteDialog({
|
||||
teamId,
|
||||
token,
|
||||
onDelete,
|
||||
children,
|
||||
}: TokenDeleteDialogProps) {
|
||||
export default function TokenDeleteDialog({ token, onDelete, children }: TokenDeleteDialogProps) {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const deleteMessage = _(msg`delete ${token.name}`);
|
||||
@ -75,7 +73,7 @@ export default function TokenDeleteDialog({
|
||||
try {
|
||||
await deleteTokenMutation({
|
||||
id: token.id,
|
||||
teamId,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
toast({
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useTransition } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
@ -38,6 +38,8 @@ import {
|
||||
import { Switch } from '@documenso/ui/primitives/switch';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
export const EXPIRATION_DATES = {
|
||||
ONE_WEEK: msg`7 days`,
|
||||
ONE_MONTH: msg`1 month`,
|
||||
@ -59,15 +61,14 @@ type NewlyCreatedToken = {
|
||||
|
||||
export type ApiTokenFormProps = {
|
||||
className?: string;
|
||||
teamId?: number;
|
||||
tokens?: Pick<ApiToken, 'id'>[];
|
||||
};
|
||||
|
||||
export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) => {
|
||||
const [isTransitionPending, startTransition] = useTransition();
|
||||
|
||||
export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => {
|
||||
const [, copy] = useCopyToClipboard();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -113,7 +114,7 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
||||
const onSubmit = async ({ tokenName, expirationDate }: TCreateTokenMutationSchema) => {
|
||||
try {
|
||||
await createTokenMutation({
|
||||
teamId,
|
||||
teamId: team?.id,
|
||||
tokenName,
|
||||
expirationDate: noExpirationDate ? null : expirationDate,
|
||||
});
|
||||
@ -238,7 +239,7 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
||||
type="submit"
|
||||
className="hidden md:inline-flex"
|
||||
disabled={!form.formState.isDirty}
|
||||
loading={form.formState.isSubmitting || isTransitionPending}
|
||||
loading={form.formState.isSubmitting}
|
||||
>
|
||||
<Trans>Create token</Trans>
|
||||
</Button>
|
||||
@ -247,7 +248,7 @@ export const ApiTokenForm = ({ className, teamId, tokens }: ApiTokenFormProps) =
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!form.formState.isDirty}
|
||||
loading={form.formState.isSubmitting || isTransitionPending}
|
||||
loading={form.formState.isSubmitting}
|
||||
>
|
||||
<Trans>Create token</Trans>
|
||||
</Button>
|
||||
|
||||
@ -3,8 +3,8 @@ import React from 'react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
export type SettingsHeaderProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
title: string | React.ReactNode;
|
||||
subtitle: string | React.ReactNode;
|
||||
hideDivider?: boolean;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
|
||||
@ -1,90 +1,115 @@
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import TokenDeleteDialog from '~/components/dialogs/token-delete-dialog';
|
||||
import { ApiTokenForm } from '~/components/forms/token';
|
||||
import { SettingsHeader } from '~/components/general/settings-header';
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
export default function ApiTokensPage() {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const { data: tokens } = trpc.apiToken.getTokens.useQuery();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">
|
||||
<Trans>API Tokens</Trans>
|
||||
</h3>
|
||||
<SettingsHeader
|
||||
title={<Trans>API Tokens</Trans>}
|
||||
subtitle={
|
||||
<Trans>
|
||||
On this page, you can create and manage API tokens. See our{' '}
|
||||
<a
|
||||
className="text-primary underline"
|
||||
href={'https://docs.documenso.com/developers/public-api'}
|
||||
target="_blank"
|
||||
>
|
||||
Documentation
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>
|
||||
On this page, you can create new API tokens and manage the existing ones. <br />
|
||||
Also see our{' '}
|
||||
<a
|
||||
className="text-primary underline"
|
||||
href={'https://docs.documenso.com/developers/public-api'}
|
||||
target="_blank"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
{team && team?.currentTeamMember.role !== TeamMemberRole.ADMIN ? (
|
||||
<Alert
|
||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
|
||||
variant="warning"
|
||||
>
|
||||
<div>
|
||||
<AlertTitle>
|
||||
<Trans>Unauthorized</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription className="mr-2">
|
||||
<Trans>You need to be an admin to manage API tokens.</Trans>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<ApiTokenForm className="max-w-xl" tokens={tokens} />
|
||||
|
||||
<hr className="my-4" />
|
||||
<hr className="mb-4 mt-8" />
|
||||
|
||||
<ApiTokenForm className="max-w-xl" tokens={tokens} />
|
||||
<h4 className="text-xl font-medium">
|
||||
<Trans>Your existing tokens</Trans>
|
||||
</h4>
|
||||
|
||||
<hr className="mb-4 mt-8" />
|
||||
|
||||
<h4 className="text-xl font-medium">
|
||||
<Trans>Your existing tokens</Trans>
|
||||
</h4>
|
||||
|
||||
{tokens && tokens.length === 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm italic">
|
||||
<Trans>Your tokens will be shown here once you create them.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tokens && tokens.length > 0 && (
|
||||
<div className="mt-4 flex max-w-xl flex-col gap-y-4">
|
||||
{tokens.map((token) => (
|
||||
<div key={token.id} className="border-border rounded-lg border p-4">
|
||||
<div className="flex items-center justify-between gap-x-4">
|
||||
<div>
|
||||
<h5 className="text-base">{token.name}</h5>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
<Trans>Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)}</Trans>
|
||||
</p>
|
||||
{token.expires ? (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<Trans>Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)}</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<Trans>Token doesn't have an expiration date</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TokenDeleteDialog token={token}>
|
||||
<Button variant="destructive">
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
</TokenDeleteDialog>
|
||||
</div>
|
||||
</div>
|
||||
{tokens && tokens.length === 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm italic">
|
||||
<Trans>Your tokens will be shown here once you create them.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tokens && tokens.length > 0 && (
|
||||
<div className="mt-4 flex max-w-xl flex-col gap-y-4">
|
||||
{tokens.map((token) => (
|
||||
<div key={token.id} className="border-border rounded-lg border p-4">
|
||||
<div className="flex items-center justify-between gap-x-4">
|
||||
<div>
|
||||
<h5 className="text-base">{token.name}</h5>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
<Trans>
|
||||
Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)}
|
||||
</Trans>
|
||||
</p>
|
||||
{token.expires ? (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<Trans>
|
||||
Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)}
|
||||
</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<Trans>Token doesn't have an expiration date</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TokenDeleteDialog token={token}>
|
||||
<Button variant="destructive">
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
</TokenDeleteDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,126 +1,3 @@
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import ApiTokensPage from '~/routes/_authenticated+/settings+/tokens+/index';
|
||||
|
||||
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import TokenDeleteDialog from '~/components/dialogs/token-delete-dialog';
|
||||
import { ApiTokenForm } from '~/components/forms/token';
|
||||
|
||||
import type { Route } from './+types/tokens';
|
||||
|
||||
// Todo: This can be optimized.
|
||||
export async function loader({ request, params }: Route.LoaderArgs) {
|
||||
const { user } = await getSession(request);
|
||||
|
||||
const team = await getTeamByUrl({
|
||||
userId: user.id,
|
||||
teamUrl: params.teamUrl,
|
||||
});
|
||||
|
||||
const tokens = await getTeamTokens({ userId: user.id, teamId: team.id }).catch(() => null);
|
||||
|
||||
return {
|
||||
user,
|
||||
team,
|
||||
tokens,
|
||||
};
|
||||
}
|
||||
|
||||
export default function ApiTokensPage({ loaderData }: Route.ComponentProps) {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const { team, tokens } = loaderData;
|
||||
|
||||
if (!tokens) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">
|
||||
<Trans>API Tokens</Trans>
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>Something went wrong.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">
|
||||
<Trans>API Tokens</Trans>
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>
|
||||
On this page, you can create new API tokens and manage the existing ones. <br />
|
||||
You can view our swagger docs{' '}
|
||||
<a
|
||||
className="text-primary underline"
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/openapi`}
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<ApiTokenForm className="max-w-xl" teamId={team.id} tokens={tokens} />
|
||||
|
||||
<hr className="mb-4 mt-8" />
|
||||
|
||||
<h4 className="text-xl font-medium">
|
||||
<Trans>Your existing tokens</Trans>
|
||||
</h4>
|
||||
|
||||
{tokens.length === 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm italic">
|
||||
<Trans>Your tokens will be shown here once you create them.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tokens.length > 0 && (
|
||||
<div className="mt-4 flex max-w-xl flex-col gap-y-4">
|
||||
{tokens.map((token) => (
|
||||
<div key={token.id} className="border-border rounded-lg border p-4">
|
||||
<div className="flex items-center justify-between gap-x-4">
|
||||
<div>
|
||||
<h5 className="text-base">{token.name}</h5>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
<Trans>Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)}</Trans>
|
||||
</p>
|
||||
{token.expires ? (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<Trans>Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)}</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
<Trans>Token doesn't have an expiration date</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TokenDeleteDialog token={token} teamId={team.id}>
|
||||
<Button variant="destructive">
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
</TokenDeleteDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ApiTokensPage;
|
||||
|
||||
Reference in New Issue
Block a user