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

feat: public api start #674

Merged
merged 50 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
24d9906
feat: public api start
catalinpit Nov 22, 2023
4a6b3ed
feat: get documents api route with pagination
catalinpit Nov 22, 2023
6d6c935
feat: update contract
catalinpit Nov 22, 2023
b3008fb
feat: add route for retrieving a single document by id
catalinpit Nov 23, 2023
5c8a77e
chore: merged main
catalinpit Nov 23, 2023
309b561
feat: create the model for the api token
catalinpit Nov 23, 2023
2ccede7
chore: update the contract to add deleteDocument route
catalinpit Nov 23, 2023
80fe7cc
feat: api token page in the settings
catalinpit Nov 24, 2023
fbee6ee
feat: api token functions
catalinpit Nov 24, 2023
2deaad5
feat: token page
catalinpit Nov 27, 2023
13997d3
feat: add delete and copy token on token page
catalinpit Nov 27, 2023
6a5fc7a
feat: confirm to delete dialog
catalinpit Nov 28, 2023
e1732de
feat: show newly created token
catalinpit Nov 28, 2023
d43d40f
feat: improvements to the newly created token message
catalinpit Nov 29, 2023
7680067
feat: improve messaging
catalinpit Nov 29, 2023
6be4b7a
feat: add authorization for api calls
catalinpit Nov 30, 2023
936e75f
chore: merged main
catalinpit Dec 6, 2023
6c5526d
chore: update routes
catalinpit Dec 6, 2023
11ae6d3
chore: small changes
catalinpit Dec 6, 2023
54401b9
chore: split api contract
catalinpit Dec 8, 2023
66c0db9
chore: cleanup and feedback implementation
catalinpit Dec 8, 2023
8ecd8a7
chore: implemented feedback + a small refactoring
catalinpit Dec 11, 2023
e79d385
Merge branch 'main' into feat/public-api
catalinpit Dec 11, 2023
19736ce
chore: implemented feedback
catalinpit Dec 14, 2023
da03fc1
chore: finishing touches
catalinpit Dec 18, 2023
17486b9
chore: refactor delete dialog
catalinpit Dec 19, 2023
fb46b09
chore: small changes
catalinpit Dec 20, 2023
a22ada5
chore: add delete cascade
catalinpit Dec 20, 2023
6a56905
chore: merged main
catalinpit Dec 21, 2023
d283cc2
chore: implemented feedback
catalinpit Dec 21, 2023
a1215df
refactor: extract api implementation to package
Mythie Dec 31, 2023
3b82ba5
chore: implemented feedback plus some restructuring
catalinpit Jan 17, 2024
b28a7f9
chore: add openapi
catalinpit Jan 19, 2024
b6aface
chore: update api description
catalinpit Jan 19, 2024
5a28eaa
feat: add recipient creation
Mythie Jan 22, 2024
98df273
feat: add field and recipient endpoints
Mythie Jan 29, 2024
e91bb78
Merge branch 'main' into feat/public-api
Mythie Feb 9, 2024
b3ba77d
feat: allow user to choose expiry date
catalinpit Feb 9, 2024
1a82740
feat: support recipient roles
Mythie Feb 12, 2024
4c5b910
chore: add examples
Mythie Feb 14, 2024
b9e5905
feat: create from template
Mythie Feb 20, 2024
22e3a79
Merge branch 'main' into feat/public-api
Mythie Feb 21, 2024
2abcdd7
feat: team api tokens
Mythie Feb 22, 2024
2a74ce0
Merge branch 'main' into feat/public-api
Mythie Feb 25, 2024
4cb0d69
fix: build errors from merge
Mythie Feb 25, 2024
c7dac2f
fix: update delete endpoint and add limits
Mythie Feb 25, 2024
6b8e11b
fix: template router update
Mythie Feb 25, 2024
a54159a
fix: add missing teamId references
Mythie Feb 26, 2024
3c51a1b
Merge branch 'main' into feat/public-api
Mythie Feb 26, 2024
4778270
fix: use template dialog to pass teamId
Mythie Feb 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
"@documenso/api": "*",
"@documenso/assets": "*",
"@documenso/ee": "*",
"@documenso/lib": "*",
Expand Down
74 changes: 74 additions & 0 deletions apps/web/src/app/(dashboard)/settings/tokens/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { DateTime } from 'luxon';

import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens';
import { Button } from '@documenso/ui/primitives/button';

import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
import { LocaleDate } from '~/components/formatter/locale-date';
import { ApiTokenForm } from '~/components/forms/token';

export default async function ApiTokensPage() {
const { user } = await getRequiredServerComponentSession();

const tokens = await getUserTokens({ userId: user.id });

return (
<div>
<h3 className="text-2xl font-semibold">API Tokens</h3>

<p className="text-muted-foreground mt-2 text-sm">
On this page, you can create new API tokens and manage the existing ones.
</p>

<hr className="my-4" />

<ApiTokenForm className="max-w-xl" />

<hr className="mb-4 mt-8" />

<h4 className="text-xl font-medium">Your existing tokens</h4>

{tokens.length === 0 && (
<div className="mb-4">
<p className="text-muted-foreground mt-2 text-sm italic">
Your tokens will be shown here once you create them.
</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">
Created on <LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
</p>
{token.expires ? (
<p className="text-muted-foreground mt-1 text-xs">
Expires on <LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
</p>
) : (
<p className="text-muted-foreground mt-1 text-xs">
Token doesn't have an expiration date
</p>
)}
</div>

<div>
<DeleteTokenDialog token={token}>
<Button variant="destructive">Delete</Button>
</DeleteTokenDialog>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}
3 changes: 3 additions & 0 deletions apps/web/src/app/api/v1/openapi/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use client';

export { OpenApiDocsPage as default } from '@documenso/api/v1/api-documentation';
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Link from 'next/link';

import {
Braces,
CreditCard,
FileSpreadsheet,
Lock,
Expand Down Expand Up @@ -98,6 +99,13 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
</Link>
</DropdownMenuItem>

<DropdownMenuItem asChild>
<Link href="/settings/tokens" className="cursor-pointer">
<Braces className="mr-2 h-4 w-4" />
API Tokens
</Link>
</DropdownMenuItem>

{isBillingEnabled && (
<DropdownMenuItem asChild>
<Link href="/settings/billing" className="cursor-pointer">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';

import { CreditCard, Lock, User, Users } from 'lucide-react';
import { Braces, CreditCard, Lock, User, Users } from 'lucide-react';
catalinpit marked this conversation as resolved.
Show resolved Hide resolved

import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
Expand Down Expand Up @@ -64,6 +64,19 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
</Button>
</Link>

<Link href="/settings/tokens">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/tokens') && 'bg-secondary',
)}
>
<Braces className="mr-2 h-5 w-5" />
API Tokens
</Button>
</Link>

{isBillingEnabled && (
<Link href="/settings/billing">
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';

import { CreditCard, Lock, User, Users } from 'lucide-react';
import { Braces, CreditCard, Lock, User, Users } from 'lucide-react';
catalinpit marked this conversation as resolved.
Show resolved Hide resolved

import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { cn } from '@documenso/ui/lib/utils';
Expand Down Expand Up @@ -67,6 +67,19 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
</Button>
</Link>

<Link href="/settings/tokens">
<Button
variant="ghost"
className={cn(
'w-full justify-start',
pathname?.startsWith('/settings/tokens') && 'bg-secondary',
)}
>
<Braces className="mr-2 h-5 w-5" />
API Tokens
</Button>
</Link>

{isBillingEnabled && (
<Link href="/settings/billing">
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const EXPIRATION_DATES = {
ONE_WEEK: '7 days',
ONE_MONTH: '1 month',
THREE_MONTHS: '3 months',
SIX_MONTHS: '6 months',
ONE_YEAR: '12 months',
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
'use client';

import { useEffect, useState } from 'react';

import { useRouter } from 'next/navigation';

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';

import type { ApiToken } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';

export type DeleteTokenDialogProps = {
token: Pick<ApiToken, 'id' | 'name'>;
onDelete?: () => void;
children?: React.ReactNode;
};

export default function DeleteTokenDialog({ token, onDelete, children }: DeleteTokenDialogProps) {
const router = useRouter();
const { toast } = useToast();

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

const deleteMessage = `delete ${token.name}`;

const ZDeleteTokenDialogSchema = z.object({
catalinpit marked this conversation as resolved.
Show resolved Hide resolved
tokenName: z.literal(deleteMessage, {
errorMap: () => ({ message: `You must enter '${deleteMessage}' to proceed` }),
}),
});
catalinpit marked this conversation as resolved.
Show resolved Hide resolved

type TDeleteTokenByIdMutationSchema = z.infer<typeof ZDeleteTokenDialogSchema>;

const { mutateAsync: deleteTokenMutation } = trpc.apiToken.deleteTokenById.useMutation({
onSuccess() {
onDelete?.();
},
});

const form = useForm<TDeleteTokenByIdMutationSchema>({
resolver: zodResolver(ZDeleteTokenDialogSchema),
values: {
tokenName: '',
},
});

const onSubmit = async () => {
try {
await deleteTokenMutation({
id: token.id,
});

toast({
title: 'Token deleted',
description: 'The token was deleted successfully.',
duration: 5000,
});

setIsOpen(false);

router.refresh();
} catch (error) {
toast({
title: 'An unknown error occurred',
variant: 'destructive',
duration: 5000,
description:
'We encountered an unknown error while attempting to delete this token. Please try again later.',
});
}
};

useEffect(() => {
catalinpit marked this conversation as resolved.
Show resolved Hide resolved
if (!isOpen) {
form.reset();
}
}, [isOpen, form]);

return (
<Dialog
open={isOpen}
onOpenChange={(value) => !form.formState.isSubmitting && setIsOpen(value)}
>
<DialogTrigger asChild={true}>
{children ?? (
<Button className="mr-4" variant="destructive">
Delete
</Button>
)}
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure you want to delete this token?</DialogTitle>

<DialogDescription>
Please note that this action is irreversible. Once confirmed, your token will be
permanently deleted.
</DialogDescription>
</DialogHeader>

<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset
className="flex h-full flex-col space-y-4"
disabled={form.formState.isSubmitting}
>
<FormField
control={form.control}
name="tokenName"
render={({ field }) => (
<FormItem>
<FormLabel>
Confirm by typing:{' '}
<span className="font-sm text-destructive font-semibold">
{deleteMessage}
</span>
</FormLabel>

<FormControl>
<Input className="bg-background" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<DialogFooter>
catalinpit marked this conversation as resolved.
Show resolved Hide resolved
<div className="flex w-full flex-nowrap gap-4">
<Button
type="button"
variant="secondary"
className="flex-1"
onClick={() => setIsOpen(false)}
>
Cancel
</Button>

<Button
type="submit"
variant="destructive"
className="flex-1"
disabled={!form.formState.isValid}
loading={form.formState.isSubmitting}
>
I'm sure! Delete it
</Button>
</div>
</DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent>
</Dialog>
);
}