Added user profile update
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { passkey } from "@/lib/auth/auth-client"
|
||||
import { authClient } from "@/lib/auth/auth-client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { KeyRoundIcon } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
@@ -22,7 +22,7 @@ export function PasskeyAdd() {
|
||||
})
|
||||
|
||||
async function handleSubmit({ name }: { name: string }) {
|
||||
const res = await passkey.addPasskey({ name })
|
||||
const res = await authClient.passkey.addPasskey({ name })
|
||||
|
||||
if (res && res.error) {
|
||||
toast.error(res.error.message)
|
||||
@@ -35,26 +35,33 @@ export function PasskeyAdd() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 mb-5 max-w-48">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? "Adding..." : "Add Passkey"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<Card className="max-w-lg">
|
||||
<CardHeader>
|
||||
<CardTitle>Passkeys</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 mb-5 max-w-48">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? "Adding..." : "Add Passkey"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,12 +71,12 @@ export function PasskeyItem({ id, name, createdAt }: { id: string, name?: string
|
||||
|
||||
async function handleDelete() {
|
||||
setIsLoading(true)
|
||||
await passkey.deletePasskey({ id })
|
||||
await authClient.passkey.deletePasskey({ id })
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="mb-2">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<p className="font-medium flex gap-2">
|
||||
<span>
|
||||
|
||||
@@ -8,9 +8,9 @@ export default async function PasskeysList() {
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 max-w-lg">
|
||||
{passkeys.length === 0 ? <p className="text-sm text-muted-foreground">No passkeys registered.</p> : (
|
||||
<div className="max-w-md">
|
||||
<div>
|
||||
{passkeys.map(passkey => (
|
||||
<PasskeyItem
|
||||
key={passkey.id}
|
||||
|
||||
@@ -10,19 +10,19 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "@/components/ui/sidebar"
|
||||
import { signOut, useSession } from "@/lib/auth/auth-client"
|
||||
import { authClient } from "@/lib/auth/auth-client"
|
||||
import { LogOut, User } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export function SidebarUserDropdown() {
|
||||
const { data: session, isPending } = useSession()
|
||||
const { data: session, isPending } = authClient.useSession()
|
||||
const { state } = useSidebar()
|
||||
const router = useRouter()
|
||||
|
||||
const handleSignOut = async () => {
|
||||
const res = await signOut({
|
||||
const res = await authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/")
|
||||
|
||||
220
src/app/(admin)/dashboard/_components/user-profile.tsx
Normal file
220
src/app/(admin)/dashboard/_components/user-profile.tsx
Normal file
@@ -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 (
|
||||
<Card className="max-w-lg">
|
||||
<CardContent className="p-6">
|
||||
<div className="animate-pulse">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-16 bg-gray-200 rounded-full"></div>
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 bg-gray-200 rounded w-32"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-24"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (!session?.user) {
|
||||
return (
|
||||
<Card className="max-w-lg">
|
||||
<CardContent className="p-6">
|
||||
<p className="text-center text-gray-500">Not authenticated</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const { user } = session
|
||||
const initials = user.name
|
||||
? user.name.split(" ").map(n => n[0]).join("").toUpperCase()
|
||||
: user.email?.[0]?.toUpperCase() || "U"
|
||||
|
||||
return (
|
||||
<Card className="max-w-lg">
|
||||
<CardHeader className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Avatar className="w-20 h-20">
|
||||
<AvatarImage src={user.image || undefined} alt={user.name || "User"} />
|
||||
<AvatarFallback className="text-lg">
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl">Profile</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
User ID
|
||||
</Label>
|
||||
<Input
|
||||
value={user.id}
|
||||
disabled
|
||||
className="bg-muted text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
Joined
|
||||
</Label>
|
||||
<Input
|
||||
value={new Date(user.createdAt).toLocaleDateString()}
|
||||
disabled
|
||||
className="bg-muted text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<UserUpdateForm name={user.name} image={user.image ? user.image : undefined} email={user.email} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const userUpdateSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
image: z.string().url().optional()
|
||||
})
|
||||
|
||||
function UserUpdateForm({ name, image, email }: z.infer<typeof userUpdateSchema> & { email: string }) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const router = useRouter()
|
||||
const form = useForm<z.infer<typeof userUpdateSchema>>({
|
||||
resolver: zodResolver(userUpdateSchema),
|
||||
defaultValues: { name, image },
|
||||
disabled: !editing
|
||||
})
|
||||
|
||||
async function handleSubmit(data: z.infer<typeof userUpdateSchema>) {
|
||||
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 (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-sm font-medium flex items-center gap-2">
|
||||
<User2 className="w-4 h-4" />
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="image"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-sm font-medium flex items-center gap-2">
|
||||
{/* eslint-disable-next-line jsx-a11y/alt-text */}
|
||||
<Image className="w-4 h-4" />
|
||||
Image
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex gap-2">
|
||||
<Input {...field} />
|
||||
<Button type="button" variant="outline" hidden={!editing} onClick={handleGravatar}>
|
||||
Gravatar
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
{editing === true ?
|
||||
(
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditing(false)
|
||||
form.reset()
|
||||
}}
|
||||
variant="outline"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
) :
|
||||
(
|
||||
<Button onClick={() => setEditing(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="pb-6">
|
||||
<h1 className="block mb-2 text-2xl font-bold text-foreground">User Profile</h1>
|
||||
<h1 className="block text-muted-foreground">Manage user settings</h1>
|
||||
</div>
|
||||
<PasskeyAdd />
|
||||
<PasskeysList />
|
||||
<div className="flex flex-col gap-6">
|
||||
<UserProfile />
|
||||
<PasskeyAdd />
|
||||
<PasskeysList />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
39
src/app/api/gravatar/[email]/route.ts
Normal file
39
src/app/api/gravatar/[email]/route.ts
Normal file
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -196,4 +196,4 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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("/")
|
||||
|
||||
@@ -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()]
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user