Added user profile update
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
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 { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input"
|
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 { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { KeyRoundIcon } from "lucide-react"
|
import { KeyRoundIcon } from "lucide-react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
@@ -22,7 +22,7 @@ export function PasskeyAdd() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function handleSubmit({ name }: { name: string }) {
|
async function handleSubmit({ name }: { name: string }) {
|
||||||
const res = await passkey.addPasskey({ name })
|
const res = await authClient.passkey.addPasskey({ name })
|
||||||
|
|
||||||
if (res && res.error) {
|
if (res && res.error) {
|
||||||
toast.error(res.error.message)
|
toast.error(res.error.message)
|
||||||
@@ -35,26 +35,33 @@ export function PasskeyAdd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Card className="max-w-lg">
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 mb-5 max-w-48">
|
<CardHeader>
|
||||||
<FormField
|
<CardTitle>Passkeys</CardTitle>
|
||||||
control={form.control}
|
</CardHeader>
|
||||||
name="name"
|
<CardContent>
|
||||||
render={({ field }) => (
|
<Form {...form}>
|
||||||
<FormItem>
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 mb-5 max-w-48">
|
||||||
<FormLabel>Name</FormLabel>
|
<FormField
|
||||||
<FormControl>
|
control={form.control}
|
||||||
<Input {...field} />
|
name="name"
|
||||||
</FormControl>
|
render={({ field }) => (
|
||||||
<FormMessage />
|
<FormItem>
|
||||||
</FormItem>
|
<FormLabel>Name</FormLabel>
|
||||||
)}
|
<FormControl>
|
||||||
/>
|
<Input {...field} />
|
||||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
</FormControl>
|
||||||
{form.formState.isSubmitting ? "Adding..." : "Add Passkey"}
|
<FormMessage />
|
||||||
</Button>
|
</FormItem>
|
||||||
</form>
|
)}
|
||||||
</Form>
|
/>
|
||||||
|
<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() {
|
async function handleDelete() {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
await passkey.deletePasskey({ id })
|
await authClient.passkey.deletePasskey({ id })
|
||||||
router.refresh()
|
router.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="mb-2">
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="font-medium flex gap-2">
|
<p className="font-medium flex gap-2">
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export default async function PasskeysList() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
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> : (
|
{passkeys.length === 0 ? <p className="text-sm text-muted-foreground">No passkeys registered.</p> : (
|
||||||
<div className="max-w-md">
|
<div>
|
||||||
{passkeys.map(passkey => (
|
{passkeys.map(passkey => (
|
||||||
<PasskeyItem
|
<PasskeyItem
|
||||||
key={passkey.id}
|
key={passkey.id}
|
||||||
|
|||||||
@@ -10,19 +10,19 @@ import {
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "@/components/ui/sidebar"
|
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 { LogOut, User } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
export function SidebarUserDropdown() {
|
export function SidebarUserDropdown() {
|
||||||
const { data: session, isPending } = useSession()
|
const { data: session, isPending } = authClient.useSession()
|
||||||
const { state } = useSidebar()
|
const { state } = useSidebar()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
const res = await signOut({
|
const res = await authClient.signOut({
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.push("/")
|
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 { getSession } from "@/lib/auth/session"
|
||||||
import { PasskeyAdd } from "../_components/passkey"
|
import { PasskeyAdd } from "../_components/passkey"
|
||||||
import PasskeysList from "../_components/passkeys-list"
|
import PasskeysList from "../_components/passkeys-list"
|
||||||
|
import UserProfile from "../_components/user-profile"
|
||||||
|
|
||||||
export default async function UserPage() {
|
export default async function UserPage() {
|
||||||
const { session, redirect } = await getSession()
|
const { session, redirect } = await getSession()
|
||||||
@@ -11,12 +12,15 @@ export default async function UserPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<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 mb-2 text-2xl font-bold text-foreground">User Profile</h1>
|
||||||
<h1 className="block text-muted-foreground">Manage user settings</h1>
|
<h1 className="block text-muted-foreground">Manage user settings</h1>
|
||||||
</div>
|
</div>
|
||||||
<PasskeyAdd />
|
<div className="flex flex-col gap-6">
|
||||||
<PasskeysList />
|
<UserProfile />
|
||||||
|
<PasskeyAdd />
|
||||||
|
<PasskeysList />
|
||||||
|
</div>
|
||||||
</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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { AuthentikIcon } from "@/components/svgs/authentik"
|
import { AuthentikIcon } from "@/components/svgs/authentik"
|
||||||
import { Button } from "@/components/ui/button"
|
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 { KeyIcon, Loader2 } from "lucide-react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
@@ -20,7 +20,7 @@ export function OAuthSignInButton({
|
|||||||
|
|
||||||
const handleOAuthSignIn = async () => {
|
const handleOAuthSignIn = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const res = await signIn.oauth2({
|
const res = await authClient.signIn.oauth2({
|
||||||
providerId: "authentik",
|
providerId: "authentik",
|
||||||
callbackURL: "/dashboard"
|
callbackURL: "/dashboard"
|
||||||
})
|
})
|
||||||
@@ -36,7 +36,7 @@ export function OAuthSignInButton({
|
|||||||
|
|
||||||
const handlePasskeySignIn = async () => {
|
const handlePasskeySignIn = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const res = await signIn.passkey({
|
const res = await authClient.signIn.passkey({
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.push("/dashboard")
|
router.push("/dashboard")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from "@/components/ui/dropdown-menu"
|
} 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 { LogOut, Settings } from "lucide-react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
@@ -21,11 +21,11 @@ interface UserDropdownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function UserDropdown({ className }: UserDropdownProps) {
|
export function UserDropdown({ className }: UserDropdownProps) {
|
||||||
const { data: session, isPending } = useSession()
|
const { data: session, isPending } = authClient.useSession()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
const res = await signOut({
|
const res = await authClient.signOut({
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.push("/")
|
router.push("/")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { genericOAuthClient, passkeyClient } from "better-auth/client/plugins"
|
import { genericOAuthClient, passkeyClient } from "better-auth/client/plugins"
|
||||||
import { createAuthClient } from "better-auth/react"
|
import { createAuthClient } from "better-auth/react"
|
||||||
|
|
||||||
export const { signIn, signOut, useSession, passkey } = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
plugins: [genericOAuthClient(), passkeyClient()]
|
plugins: [genericOAuthClient(), passkeyClient()]
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user