diff --git a/src/app/(admin)/dashboard/_components/passkeys-list.tsx b/src/app/(admin)/dashboard/_components/passkeys-list.tsx
index 92d6d51..8c4b01a 100644
--- a/src/app/(admin)/dashboard/_components/passkeys-list.tsx
+++ b/src/app/(admin)/dashboard/_components/passkeys-list.tsx
@@ -8,9 +8,9 @@ export default async function PasskeysList() {
})
return (
-
+
{passkeys.length === 0 ?
No passkeys registered.
: (
-
+
{passkeys.map(passkey => (
{
- const res = await signOut({
+ const res = await authClient.signOut({
fetchOptions: {
onSuccess: () => {
router.push("/")
diff --git a/src/app/(admin)/dashboard/_components/user-profile.tsx b/src/app/(admin)/dashboard/_components/user-profile.tsx
new file mode 100644
index 0000000..683c37d
--- /dev/null
+++ b/src/app/(admin)/dashboard/_components/user-profile.tsx
@@ -0,0 +1,220 @@
+"use client"
+
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { authClient } from "@/lib/auth/auth-client"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Calendar, Image, User, User2 } from "lucide-react"
+import { useRouter } from "next/navigation"
+import { useState } from "react"
+import { useForm } from "react-hook-form"
+import { toast } from "sonner"
+import { z } from "zod"
+
+export default function UserProfile() {
+ const { data: session, isPending } = authClient.useSession()
+
+ if (isPending) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ if (!session?.user) {
+ return (
+
+
+ Not authenticated
+
+
+ )
+ }
+
+ const { user } = session
+ const initials = user.name
+ ? user.name.split(" ").map(n => n[0]).join("").toUpperCase()
+ : user.email?.[0]?.toUpperCase() || "U"
+
+ return (
+
+
+
+
+ Profile
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const userUpdateSchema = z.object({
+ name: z.string().min(1),
+ image: z.string().url().optional()
+})
+
+function UserUpdateForm({ name, image, email }: z.infer & { email: string }) {
+ const [editing, setEditing] = useState(false)
+ const router = useRouter()
+ const form = useForm>({
+ resolver: zodResolver(userUpdateSchema),
+ defaultValues: { name, image },
+ disabled: !editing
+ })
+
+ async function handleSubmit(data: z.infer) {
+ const res = await authClient.updateUser({
+ name: data.name,
+ image: data.image
+ })
+
+ if (res && res.error) {
+ toast.error(res.error.message)
+ return
+ }
+
+ toast.success("Profile updated successfully")
+ setEditing(false)
+ router.refresh()
+ }
+
+ async function handleGravatar() {
+ const res = await fetch(`/api/gravatar/${email}`)
+
+ if (!res.ok) {
+ toast.error("Failed to fetch Gravatar")
+ return
+ }
+
+ const data = await res.json()
+
+ if (data.error) {
+ toast.error(data.message)
+ return
+ }
+
+ console.log(data)
+
+ form.setValue("image", data.url)
+ }
+
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/src/app/(admin)/dashboard/user/page.tsx b/src/app/(admin)/dashboard/user/page.tsx
index 98f8cc2..d39de94 100644
--- a/src/app/(admin)/dashboard/user/page.tsx
+++ b/src/app/(admin)/dashboard/user/page.tsx
@@ -1,6 +1,7 @@
import { getSession } from "@/lib/auth/session"
import { PasskeyAdd } from "../_components/passkey"
import PasskeysList from "../_components/passkeys-list"
+import UserProfile from "../_components/user-profile"
export default async function UserPage() {
const { session, redirect } = await getSession()
@@ -11,12 +12,15 @@ export default async function UserPage() {
return (
-
+
User Profile
Manage user settings
-
-
+
)
}
diff --git a/src/app/api/gravatar/[email]/route.ts b/src/app/api/gravatar/[email]/route.ts
new file mode 100644
index 0000000..d73aa14
--- /dev/null
+++ b/src/app/api/gravatar/[email]/route.ts
@@ -0,0 +1,39 @@
+import { getSession } from "@/lib/auth/session"
+import { getGravatar } from "@/lib/gravatar"
+import { NextResponse } from "next/server"
+import { z } from "zod"
+
+export async function GET(_: Request, { params }: { params: Promise<{ email: string }> }) {
+ const { session } = await getSession()
+ const { email: unsafeEmail } = await params
+
+ if (!session) {
+ return NextResponse.json({
+ error: true,
+ message: "Unauthorized"
+ }, { status: 401 })
+ }
+
+ const { error, data: email } = z.string().email().safeParse(unsafeEmail)
+
+ if (error) {
+ return NextResponse.json({
+ error: true,
+ message: "Invalid email"
+ }, { status: 400 })
+ }
+
+ const gravatar = await getGravatar(email)
+
+ if (!gravatar) {
+ return NextResponse.json({
+ error: true,
+ message: "Gravatar not found"
+ }, { status: 404 })
+ }
+
+ return NextResponse.json({
+ error: false,
+ url: gravatar
+ })
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 1a6e594..8aee68a 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -196,4 +196,4 @@
body {
@apply bg-background text-foreground;
}
-}
+}
\ No newline at end of file
diff --git a/src/app/sign-in/_components/signin-button.tsx b/src/app/sign-in/_components/signin-button.tsx
index f0d16d4..06f4cc0 100644
--- a/src/app/sign-in/_components/signin-button.tsx
+++ b/src/app/sign-in/_components/signin-button.tsx
@@ -2,7 +2,7 @@
import { AuthentikIcon } from "@/components/svgs/authentik"
import { Button } from "@/components/ui/button"
-import { signIn } from "@/lib/auth/auth-client"
+import { authClient } from "@/lib/auth/auth-client"
import { KeyIcon, Loader2 } from "lucide-react"
import { useRouter } from "next/navigation"
import { useState } from "react"
@@ -20,7 +20,7 @@ export function OAuthSignInButton({
const handleOAuthSignIn = async () => {
setIsLoading(true)
- const res = await signIn.oauth2({
+ const res = await authClient.signIn.oauth2({
providerId: "authentik",
callbackURL: "/dashboard"
})
@@ -36,7 +36,7 @@ export function OAuthSignInButton({
const handlePasskeySignIn = async () => {
setIsLoading(true)
- const res = await signIn.passkey({
+ const res = await authClient.signIn.passkey({
fetchOptions: {
onSuccess: () => {
router.push("/dashboard")
diff --git a/src/components/user-dropdown.tsx b/src/components/user-dropdown.tsx
index cbc10ac..3b6240d 100644
--- a/src/components/user-dropdown.tsx
+++ b/src/components/user-dropdown.tsx
@@ -10,7 +10,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
-import { signOut, useSession } from "@/lib/auth/auth-client"
+import { authClient } from "@/lib/auth/auth-client"
import { LogOut, Settings } from "lucide-react"
import Link from "next/link"
import { useRouter } from "next/navigation"
@@ -21,11 +21,11 @@ interface UserDropdownProps {
}
export function UserDropdown({ className }: UserDropdownProps) {
- const { data: session, isPending } = useSession()
+ const { data: session, isPending } = authClient.useSession()
const router = useRouter()
const handleSignOut = async () => {
- const res = await signOut({
+ const res = await authClient.signOut({
fetchOptions: {
onSuccess: () => {
router.push("/")
diff --git a/src/lib/auth/auth-client.ts b/src/lib/auth/auth-client.ts
index c2b891e..df89d31 100644
--- a/src/lib/auth/auth-client.ts
+++ b/src/lib/auth/auth-client.ts
@@ -1,6 +1,6 @@
import { genericOAuthClient, passkeyClient } from "better-auth/client/plugins"
import { createAuthClient } from "better-auth/react"
-export const { signIn, signOut, useSession, passkey } = createAuthClient({
+export const authClient = createAuthClient({
plugins: [genericOAuthClient(), passkeyClient()]
})