diff --git a/src/features/profile/profile-account.tsx b/src/features/profile/profile-account.tsx index 1ad37285..c153487e 100644 --- a/src/features/profile/profile-account.tsx +++ b/src/features/profile/profile-account.tsx @@ -2,16 +2,8 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { - AlertCircle, - Check, - Copy, - KeyRound, - Loader2, - Plus, - Trash2, -} from "lucide-react"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { AlertCircle, Loader2 } from "lucide-react"; +import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; import { authClient } from "@/lib/auth/auth-client"; @@ -27,24 +19,7 @@ import { } from "@/components/ui/form"; import { EmailSchema, EmailSchemaType } from "./account.schema"; import { BetterAuthError } from "@/types/auth"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; -import { useState } from "react"; -import { timeAgo } from "@/utils/date-formatting"; -import { - createApiKeysAction, - deleteApiKeyAction, - getApiKeysAction, -} from "@/features/profile/profile.action"; -import { copyToClipboardWithMeta } from "@/components/common/copy-button"; +import { ProfileApiKeys } from "./profile-api-keys"; interface ProfileAccountProps { user: User; @@ -52,15 +27,8 @@ interface ProfileAccountProps { } export function ProfileAccount({ user, apiEnabled }: ProfileAccountProps) { - const router = useRouter(); - const [isAddApiKeyOpen, setIsAddApiKeyOpen] = useState(false); - const [apiKeyName, setApiKeyName] = useState(""); - - const [createdApiKey, setCreatedApiKey] = useState(null); - const [copiedApiKey, setCopiedApiKey] = useState(false); - const emailForm = useZodForm({ schema: EmailSchema, defaultValues: { @@ -74,31 +42,21 @@ export function ProfileAccount({ user, apiEnabled }: ProfileAccountProps) { newEmail: values.email, callbackURL: window.location.href, }); - if (error) throw error; - return values.email; }, onSuccess: (newEmail) => { toast.success("Email updated successfully."); - emailForm.reset({ email: newEmail }); - router.refresh(); }, onError: (error: BetterAuthError) => { if (error.code === "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL") { - toast.error( - "User already exists, use another email address!", - ); - + toast.error("User already exists, use another email address!"); emailForm.reset({ email: user.email }); - router.refresh(); } else { - toast.error( - "An error occurred while trying to update your password!", - ); + toast.error("An error occurred while trying to update your password!"); } }, }); @@ -109,23 +67,19 @@ export function ProfileAccount({ user, apiEnabled }: ProfileAccountProps) { } = useMutation({ mutationFn: async () => { const currentEmailInput = emailForm.getValues("email"); - let error: BetterAuthError | null = null; if (currentEmailInput === user.email) { - const sendVerification = - await authClient.sendVerificationEmail({ - email: currentEmailInput, - callbackURL: window.location.href, - }); - + const sendVerification = await authClient.sendVerificationEmail({ + email: currentEmailInput, + callbackURL: window.location.href, + }); error = sendVerification.error; } else { const result = await authClient.changeEmail({ callbackURL: window.location.href, newEmail: currentEmailInput, }); - error = result.error; } @@ -139,405 +93,90 @@ export function ProfileAccount({ user, apiEnabled }: ProfileAccountProps) { }, }); - const { - data: apikeys, - isLoading: isLoadingApiKeys, - refetch: refetchApiKeys, - } = useQuery({ - queryKey: ["apikeys"], - queryFn: async () => { - const result = await getApiKeysAction(); - - if (result?.data?.success) { - return result.data.value; - } - - - - throw new Error("Failed to fetch API Keys"); - }, - }); - - const { mutate: addApiKey, isPending: isAddingApikey } = useMutation({ - mutationFn: async () => { - - const result = await createApiKeysAction({ - name: apiKeyName || "My API Key" - }); - console.log(result); - if (!result?.data?.success) { - throw new Error("Failed to create API Key"); - } - - return result; - }, - - onSuccess: (result: any) => { - setCreatedApiKey(result.data.value.key); - - toast.success("API Key created successfully"); - - setIsAddApiKeyOpen(false); - setApiKeyName(""); - - refetchApiKeys(); - }, - - onError: (error: any) => { - toast.error(error.message || "Failed to create API Key"); - }, - }); - - const { mutate: revokeApiKey, isPending: isRevokingApiKey } = - useMutation({ - mutationFn: async (id: string) => { - const result = await deleteApiKeyAction({ id }); - - if (!result?.data?.success) { - throw new Error("Failed to revoke API Key"); - } - }, - - onSuccess: () => { - toast.success("API Key revoked successfully"); - - refetchApiKeys(); - }, - - onError: () => { - toast.error("Failed to revoke API Key"); - }, - }); - - const handleCopyApiKey = async () => { - if (!createdApiKey) return; - - await copyToClipboardWithMeta(createdApiKey); - - setCopiedApiKey(true); - - toast.success("API Key copied"); - - setTimeout(() => { - setCopiedApiKey(false); - }, 2000); - }; - return ( - <> -
-
-

- Account Settings -

+
+
+

+ Account Settings +

+

+ Update your email and preferences. +

+
-

- Update your email and preferences. -

-
+
+
updateEmail(values)}> +
+ ( + + Email Address -
- updateEmail(values)} - > -
- ( - - - Email Address - +
+
+ + + -
-
- - - +
+ -
+ {!user.emailVerified && ( - - {!user.emailVerified && ( - - )} -
+ )}
- -
- - )} - /> - {!user.emailVerified && ( -
- - - - Your email is not verified. Please check - your inbox. - -
- )} -
- -
- - {apiEnabled === true && ( -
-
-
-

- API Keys -

- -
- Manage your personal access tokens for API - authentication -
-
- - - - - - - - - - Add New API Key - - - - Give a name to your API Key to identify - it later. - - - -
-
- - - - setApiKeyName(e.target.value) - } - /> +
-
+ + )} + /> - - - - - -
-
-
- -
- {isLoadingApiKeys ? ( -
- -
- ) : apikeys && apikeys.length > 0 ? ( - apikeys.map((ak: any) => ( - revokeApiKey(id)} - isRevoking={isRevokingApiKey} - /> - )) - ) : ( -
- No API Key found. + {!user.emailVerified && ( +
+ + + Your email is not verified. Please check your inbox. +
)}
-
- )} +
- { - if (!open) { - setCreatedApiKey(null); - } - }} - > - - - Your API Key - - - This API Key will only be displayed once. -
- Copy it now before closing this dialog. -
- For security reasons, it cannot be viewed again. -
-
- -
-
- - - -
- -
- Store this API Key securely. You will not be able - to see it again after closing this dialog. -
-
- - - - -
-
- - ); -} - -function ApiKeyRow({ - apikey, - onRevoke, - isRevoking, - }: { - apikey: any; - onRevoke: (id: string) => void; - isRevoking: boolean; -}) { - return ( -
-
-
- -
- -
-
- {apikey.name || "Unnamed API Key"} -
- - {apikey?.start && apikey?.prefix ? ( -
- {apikey.start}•••••••• -
- ) : null} - -
- Created {timeAgo(new Date(apikey.createdAt))} -
-
-
- - + {apiEnabled && }
); -} \ No newline at end of file +} diff --git a/src/features/profile/profile-api-keys.tsx b/src/features/profile/profile-api-keys.tsx new file mode 100644 index 00000000..68e38869 --- /dev/null +++ b/src/features/profile/profile-api-keys.tsx @@ -0,0 +1,282 @@ +"use client"; + +import {Button} from "@/components/ui/button"; +import {Input} from "@/components/ui/input"; +import {Check, Copy, KeyRound, Loader2, Plus, Trash2} from "lucide-react"; +import {useMutation, useQuery} from "@tanstack/react-query"; +import {toast} from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import {Label} from "@/components/ui/label"; +import {useState} from "react"; +import {timeAgo} from "@/utils/date-formatting"; +import { + createApiKeysAction, + deleteApiKeyAction, + getApiKeysAction, +} from "@/features/profile/profile.action"; +import {copyToClipboardWithMeta} from "@/components/common/copy-button"; +import Link from "next/link"; + +export function ProfileApiKeys() { + const [isAddApiKeyOpen, setIsAddApiKeyOpen] = useState(false); + const [apiKeyName, setApiKeyName] = useState(""); + const [createdApiKey, setCreatedApiKey] = useState(null); + const [copiedApiKey, setCopiedApiKey] = useState(false); + + const { + data: apikeys, + isLoading: isLoadingApiKeys, + refetch: refetchApiKeys, + } = useQuery({ + queryKey: ["apikeys"], + queryFn: async () => { + const result = await getApiKeysAction(); + if (result?.data?.success) return result.data.value; + throw new Error("Failed to fetch API Keys"); + }, + }); + + const {mutate: addApiKey, isPending: isAddingApikey} = useMutation({ + mutationFn: async () => { + const result = await createApiKeysAction({ + name: apiKeyName || "My API Key", + }); + if (!result?.data?.success) throw new Error("Failed to create API Key"); + return result; + }, + onSuccess: (result: any) => { + setCreatedApiKey(result.data.value.key); + toast.success("API Key created successfully"); + setIsAddApiKeyOpen(false); + setApiKeyName(""); + refetchApiKeys(); + }, + onError: (error: any) => { + toast.error(error.message || "Failed to create API Key"); + }, + }); + + const {mutate: revokeApiKey, isPending: isRevokingApiKey} = useMutation({ + mutationFn: async (id: string) => { + const result = await deleteApiKeyAction({id}); + if (!result?.data?.success) throw new Error("Failed to revoke API Key"); + }, + onSuccess: () => { + toast.success("API Key revoked successfully"); + refetchApiKeys(); + }, + onError: () => { + toast.error("Failed to revoke API Key"); + }, + }); + + const handleCopyApiKey = async () => { + if (!createdApiKey) return; + await copyToClipboardWithMeta(createdApiKey); + setCopiedApiKey(true); + toast.success("API Key copied"); + setTimeout(() => setCopiedApiKey(false), 2000); + }; + + return ( + <> +
+
+
+

API Keys

+
+ Manage your personal access tokens for API authentication.{" "} + + View docs + +
+
+ + + + + + + + + Add New API Key + + Give a name to your API Key to identify it later. + + + +
+
+ + setApiKeyName(e.target.value)} + /> +
+
+ + + + + +
+
+
+ +
+ {isLoadingApiKeys ? ( +
+ +
+ ) : apikeys && apikeys.length > 0 ? ( + apikeys.map((ak: any) => ( + revokeApiKey(id)} + isRevoking={isRevokingApiKey} + /> + )) + ) : ( +
+ No API Key found. +
+ )} +
+
+ + { + if (!open) setCreatedApiKey(null); + }} + > + e.preventDefault()}> + + Your API Key + + This API Key will only be displayed once. +
+ Copy it now before closing this dialog. +
+ For security reasons, it cannot be viewed again. +
+
+ +
+
+ + +
+ +
+ Store this API Key securely. You will not be able to see it + again after closing this dialog. +
+
+ + + + +
+
+ + ); +} + +function ApiKeyRow({ + apikey, + onRevoke, + isRevoking, + }: { + apikey: any; + onRevoke: (id: string) => void; + isRevoking: boolean; +}) { + return ( +
+
+
+ +
+ +
+
+ {apikey.name || "Unnamed API Key"} +
+ + {apikey?.start && apikey?.prefix ? ( +
+ {apikey.start}•••••••• +
+ ) : null} + +
+ Created {timeAgo(new Date(apikey.createdAt))} +
+
+
+ + +
+ ); +}