Skip to content

Commit 41c91f9

Browse files
authored
♻️ Refactor API token management to use tRPC (baptisteArno#2305)
1 parent 842f8ef commit 41c91f9

File tree

14 files changed

+181
-149
lines changed

14 files changed

+181
-149
lines changed

apps/builder/src/components/TimeSince.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { T } from "@tolgee/react";
22

33
type Props = {
4-
date: string;
4+
date: string | Date;
55
};
66

77
export const TimeSince = ({ date }: Props) => {
8-
const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
8+
const seconds = Math.floor(
9+
(Date.now() - (date instanceof Date ? date : new Date(date)).getTime()) /
10+
1000,
11+
);
912

1013
let interval = seconds / 31536000;
1114

apps/builder/src/features/user/components/ApiTokensList.tsx

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
1+
import { useMutation } from "@tanstack/react-query";
12
import { T, useTranslate } from "@tolgee/react";
23
import { byId, isDefined } from "@typebot.io/lib/utils";
34
import { Button } from "@typebot.io/ui/components/Button";
45
import { Checkbox } from "@typebot.io/ui/components/Checkbox";
56
import { Skeleton } from "@typebot.io/ui/components/Skeleton";
67
import { Table } from "@typebot.io/ui/components/Table";
78
import { useOpenControls } from "@typebot.io/ui/hooks/useOpenControls";
8-
import type { ClientUser } from "@typebot.io/user/schemas";
99
import { useState } from "react";
1010
import { ConfirmDialog } from "@/components/ConfirmDialog";
1111
import { TimeSince } from "@/components/TimeSince";
12+
import { trpc } from "@/lib/queryClient";
1213
import { toast } from "@/lib/toast";
1314
import { useApiTokens } from "../hooks/useApiTokens";
14-
import { deleteApiTokenQuery } from "../queries/deleteApiTokenQuery";
15-
import type { ApiTokenFromServer } from "../types";
1615
import { CreateApiTokenDialog } from "./CreateApiTokenDialog";
1716

18-
type Props = { user: ClientUser };
19-
20-
export const ApiTokensList = ({ user }: Props) => {
17+
export const ApiTokensList = () => {
2118
const { t } = useTranslate();
22-
const { apiTokens, isLoading, mutate } = useApiTokens({
23-
userId: user.id,
19+
const { apiTokens, isLoading, refetch } = useApiTokens({
2420
onError: (e) =>
2521
toast({
2622
title: "Failed to fetch tokens",
@@ -34,16 +30,17 @@ export const ApiTokensList = ({ user }: Props) => {
3430
} = useOpenControls();
3531
const [deletingId, setDeletingId] = useState<string>();
3632

37-
const refreshListWithNewToken = (token: ApiTokenFromServer) => {
38-
if (!apiTokens) return;
39-
mutate({ apiTokens: [token, ...apiTokens] });
40-
};
33+
const { mutate: deleteToken } = useMutation(
34+
trpc.user.deleteApiToken.mutationOptions({
35+
onSuccess: () => {
36+
refetch();
37+
setDeletingId(undefined);
38+
},
39+
}),
40+
);
4141

42-
const deleteToken = async (tokenId?: string) => {
43-
if (!apiTokens || !tokenId) return;
44-
const { error } = await deleteApiTokenQuery({ userId: user.id, tokenId });
45-
if (!error)
46-
mutate({ apiTokens: apiTokens.filter((t) => t.id !== tokenId) });
42+
const handleTokenCreated = () => {
43+
refetch();
4744
};
4845

4946
return (
@@ -55,9 +52,8 @@ export const ApiTokensList = ({ user }: Props) => {
5552
{t("account.apiTokens.createButton.label")}
5653
</Button>
5754
<CreateApiTokenDialog
58-
userId={user.id}
5955
isOpen={isCreateOpen}
60-
onNewToken={refreshListWithNewToken}
56+
onNewToken={handleTokenCreated}
6157
onClose={onCreateClose}
6258
/>
6359
</div>
@@ -107,7 +103,7 @@ export const ApiTokensList = ({ user }: Props) => {
107103
</Table.Root>
108104
<ConfirmDialog
109105
isOpen={isDefined(deletingId)}
110-
onConfirm={() => deleteToken(deletingId)}
106+
onConfirm={() => deletingId && deleteToken({ tokenId: deletingId })}
111107
onClose={() => setDeletingId(undefined)}
112108
actionType="destructive"
113109
confirmButtonLabel={t("account.apiTokens.deleteButton.label")}

apps/builder/src/features/user/components/CreateApiTokenDialog.tsx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
1+
import { useMutation } from "@tanstack/react-query";
12
import { useTranslate } from "@tolgee/react";
23
import { Button } from "@typebot.io/ui/components/Button";
34
import { Dialog } from "@typebot.io/ui/components/Dialog";
45
import { Input } from "@typebot.io/ui/components/Input";
56
import type { FormEvent } from "react";
67
import { useRef, useState } from "react";
78
import { CopyInput } from "@/components/inputs/CopyInput";
8-
import { createApiTokenQuery } from "../queries/createApiTokenQuery";
9-
import type { ApiTokenFromServer } from "../types";
9+
import { trpc } from "@/lib/queryClient";
1010

1111
type Props = {
12-
userId: string;
1312
isOpen: boolean;
14-
onNewToken: (token: ApiTokenFromServer) => void;
13+
onNewToken: () => void;
1514
onClose: () => void;
1615
};
1716

1817
const ANIMATION_DURATION = 150;
1918

2019
export const CreateApiTokenDialog = ({
21-
userId,
2220
isOpen,
2321
onClose,
2422
onNewToken,
2523
}: Props) => {
2624
const inputRef = useRef<HTMLInputElement>(null);
2725
const { t } = useTranslate();
2826
const [name, setName] = useState("");
29-
const [isSubmitting, setIsSubmitting] = useState(false);
3027
const [newTokenValue, setNewTokenValue] = useState<string>();
3128

32-
const createToken = async (e: FormEvent) => {
29+
const { mutate: createToken, isPending: isSubmitting } = useMutation(
30+
trpc.user.createApiToken.mutationOptions({
31+
onSuccess: (data) => {
32+
setNewTokenValue(data.apiToken.token);
33+
onNewToken();
34+
},
35+
}),
36+
);
37+
38+
const handleSubmit = (e: FormEvent) => {
3339
e.preventDefault();
34-
setIsSubmitting(true);
35-
const { data } = await createApiTokenQuery(userId, { name });
36-
if (data?.apiToken) {
37-
setNewTokenValue(data.apiToken.token);
38-
onNewToken(data.apiToken);
39-
}
40-
setIsSubmitting(false);
40+
createToken({ name });
4141
};
4242

4343
const handleClose = () => {
@@ -51,7 +51,7 @@ export const CreateApiTokenDialog = ({
5151
return (
5252
<Dialog.Root isOpen={isOpen} onClose={handleClose}>
5353
<Dialog.Popup
54-
render={<form onSubmit={createToken} />}
54+
render={<form onSubmit={handleSubmit} />}
5555
initialFocus={inputRef}
5656
>
5757
<Dialog.Title>
@@ -87,7 +87,7 @@ export const CreateApiTokenDialog = ({
8787
{newTokenValue ? null : (
8888
<Button
8989
disabled={name.length === 0 || isSubmitting}
90-
onClick={createToken}
90+
onClick={handleSubmit}
9191
type="submit"
9292
>
9393
{t("account.apiTokens.createModal.createButton.label")}

apps/builder/src/features/user/components/MyAccountForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export const MyAccountForm = () => {
7676
/>
7777
</div>
7878
)}
79-
{user && <ApiTokensList user={user} />}
79+
{user && <ApiTokensList />}
8080
</div>
8181
);
8282
};
Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,16 @@
1-
import useSWR from "swr";
2-
import { fetcher } from "@/helpers/fetcher";
3-
import type { ApiTokenFromServer } from "../types";
4-
5-
type ServerResponse = {
6-
apiTokens: ApiTokenFromServer[];
7-
};
1+
import { useQuery } from "@tanstack/react-query";
2+
import { trpc } from "@/lib/queryClient";
83

94
export const useApiTokens = ({
10-
userId,
115
onError,
126
}: {
13-
userId?: string;
14-
onError: (error: Error) => void;
7+
onError: (error: { message: string }) => void;
158
}) => {
16-
const { data, error, mutate } = useSWR<ServerResponse, Error>(
17-
userId ? `/api/users/${userId}/api-tokens` : null,
18-
fetcher,
19-
{
20-
dedupingInterval: undefined,
21-
},
22-
);
9+
const { data, error, refetch } = useQuery(trpc.user.listApiTokens.queryOptions());
2310
if (error) onError(error);
2411
return {
2512
apiTokens: data?.apiTokens,
2613
isLoading: !error && !data,
27-
mutate,
14+
refetch,
2815
};
2916
};

apps/builder/src/features/user/queries/createApiTokenQuery.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

apps/builder/src/features/user/queries/deleteApiTokenQuery.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { generateId } from "@typebot.io/lib/utils";
2+
import prisma from "@typebot.io/prisma";
3+
import { z } from "@typebot.io/zod";
4+
import { authenticatedProcedure } from "@/helpers/server/trpc";
5+
6+
const apiTokenWithTokenSchema = z.object({
7+
id: z.string(),
8+
name: z.string(),
9+
createdAt: z.date(),
10+
token: z.string(),
11+
});
12+
13+
export const createApiToken = authenticatedProcedure
14+
.meta({
15+
openapi: {
16+
method: "POST",
17+
path: "/v1/users/me/api-tokens",
18+
tags: ["User"],
19+
protect: true,
20+
},
21+
})
22+
.input(
23+
z.object({
24+
name: z.string(),
25+
}),
26+
)
27+
.output(
28+
z.object({
29+
apiToken: apiTokenWithTokenSchema,
30+
}),
31+
)
32+
.mutation(async ({ ctx: { user }, input: { name } }) => {
33+
const apiToken = await prisma.apiToken.create({
34+
data: { name, ownerId: user.id, token: generateId(24) },
35+
});
36+
return {
37+
apiToken: {
38+
id: apiToken.id,
39+
name: apiToken.name,
40+
createdAt: apiToken.createdAt,
41+
token: apiToken.token,
42+
},
43+
};
44+
});
45+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { TRPCError } from "@trpc/server";
2+
import prisma from "@typebot.io/prisma";
3+
import { z } from "@typebot.io/zod";
4+
import { authenticatedProcedure } from "@/helpers/server/trpc";
5+
6+
const apiTokenSchema = z.object({
7+
id: z.string(),
8+
name: z.string(),
9+
createdAt: z.date(),
10+
token: z.string(),
11+
ownerId: z.string(),
12+
});
13+
14+
export const deleteApiToken = authenticatedProcedure
15+
.meta({
16+
openapi: {
17+
method: "DELETE",
18+
path: "/v1/users/me/api-tokens/{tokenId}",
19+
tags: ["User"],
20+
protect: true,
21+
},
22+
})
23+
.input(
24+
z.object({
25+
tokenId: z.string(),
26+
}),
27+
)
28+
.output(
29+
z.object({
30+
apiToken: apiTokenSchema,
31+
}),
32+
)
33+
.mutation(async ({ input: { tokenId }, ctx: { user } }) => {
34+
const existingToken = await prisma.apiToken.findUnique({
35+
where: { id: tokenId },
36+
select: { ownerId: true },
37+
});
38+
39+
if (!existingToken || existingToken.ownerId !== user.id)
40+
throw new TRPCError({
41+
code: "NOT_FOUND",
42+
message: "API token not found",
43+
});
44+
45+
const apiToken = await prisma.apiToken.delete({
46+
where: { id: tokenId },
47+
});
48+
return { apiToken };
49+
});
50+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import prisma from "@typebot.io/prisma";
2+
import { z } from "@typebot.io/zod";
3+
import { authenticatedProcedure } from "@/helpers/server/trpc";
4+
5+
const apiTokenSchema = z.object({
6+
id: z.string(),
7+
name: z.string(),
8+
createdAt: z.date(),
9+
});
10+
11+
export const listApiTokens = authenticatedProcedure
12+
.meta({
13+
openapi: {
14+
method: "GET",
15+
path: "/v1/users/me/api-tokens",
16+
tags: ["User"],
17+
protect: true,
18+
},
19+
})
20+
.input(z.void())
21+
.output(
22+
z.object({
23+
apiTokens: z.array(apiTokenSchema),
24+
}),
25+
)
26+
.query(async ({ ctx: { user } }) => {
27+
const apiTokens = await prisma.apiToken.findMany({
28+
where: { ownerId: user.id },
29+
select: {
30+
id: true,
31+
name: true,
32+
createdAt: true,
33+
},
34+
orderBy: { createdAt: "desc" },
35+
});
36+
return { apiTokens };
37+
});

0 commit comments

Comments
 (0)