Move to Next Auth (#19)
This commit is contained in:
commit
615e8e6384
18 changed files with 491 additions and 600 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import { kv } from '@vercel/kv'
|
import { kv } from '@vercel/kv'
|
||||||
import { currentUser } from '@clerk/nextjs'
|
import { auth } from '@/auth'
|
||||||
|
|
||||||
import { type Chat } from '@/lib/types'
|
import { type Chat } from '@/lib/types'
|
||||||
|
|
||||||
|
|
@ -44,41 +44,50 @@ export async function getChat(id: string, userId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeChat({ id, path }: { id: string; path: string }) {
|
export async function removeChat({ id, path }: { id: string; path: string }) {
|
||||||
const user = await currentUser()
|
const session = await auth()
|
||||||
|
|
||||||
if (!user) {
|
if (!session) {
|
||||||
throw new Error('Unauthorized')
|
throw new Error('Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
const uid = await kv.hget<string>(`chat:${id}`, 'userId')
|
const uid = await kv.hget<string>(`chat:${id}`, 'userId')
|
||||||
|
|
||||||
if (uid !== user.id) {
|
if (uid !== session?.user?.id) {
|
||||||
throw new Error('Unauthorized')
|
throw new Error('Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
await kv.del(`chat:${id}`)
|
await kv.del(`chat:${id}`)
|
||||||
await kv.zrem(`user:chat:${user.id}`, `chat:${id}`)
|
await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`)
|
||||||
|
|
||||||
revalidatePath('/')
|
revalidatePath('/')
|
||||||
revalidatePath(path)
|
revalidatePath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearChats() {
|
export async function clearChats() {
|
||||||
const user = await currentUser()
|
const session = await auth()
|
||||||
|
|
||||||
if (!user) {
|
if (!session) {
|
||||||
throw new Error('Unauthorized')
|
throw new Error('Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
const chats: string[] = await kv.zrange(`user:chat:${user.id}`, 0, -1, {
|
if (!session.user?.id) {
|
||||||
rev: true
|
throw new Error('Unauthorized')
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const chats: string[] = await kv.zrange(
|
||||||
|
`user:chat:${session.user.id}`,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
{
|
||||||
|
rev: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const pipeline = kv.pipeline()
|
const pipeline = kv.pipeline()
|
||||||
|
|
||||||
for (const chat of chats) {
|
for (const chat of chats) {
|
||||||
pipeline.del(chat)
|
pipeline.del(chat)
|
||||||
pipeline.zrem(`user:chat:${user.id}`, chat)
|
pipeline.zrem(`user:chat:${session.user.id}`, chat)
|
||||||
}
|
}
|
||||||
|
|
||||||
await pipeline.exec()
|
await pipeline.exec()
|
||||||
|
|
@ -97,9 +106,17 @@ export async function getSharedChat(id: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function shareChat(chat: Chat) {
|
export async function shareChat(chat: Chat) {
|
||||||
const user = await currentUser()
|
const session = await auth()
|
||||||
|
|
||||||
if (!user || chat.userId !== user.id) {
|
if (!session) {
|
||||||
|
throw new Error('Unauthorized')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.user?.id) {
|
||||||
|
throw new Error('Unauthorized')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chat.userId !== session.user?.id) {
|
||||||
throw new Error('Unauthorized')
|
throw new Error('Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
app/api/auth/[...nextauth]/route.ts
Normal file
2
app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { GET, POST } from '@/auth'
|
||||||
|
export const runtime = 'edge'
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { kv } from '@vercel/kv'
|
import { kv } from '@vercel/kv'
|
||||||
import { OpenAIStream, StreamingTextResponse } from 'ai-connector'
|
import { OpenAIStream, StreamingTextResponse } from 'ai-connector'
|
||||||
import { Configuration, OpenAIApi } from 'openai-edge'
|
import { Configuration, OpenAIApi } from 'openai-edge'
|
||||||
|
|
||||||
import { nanoid } from '@/lib/utils'
|
import { nanoid } from '@/lib/utils'
|
||||||
import { currentUser } from '@clerk/nextjs'
|
import { auth } from '@/auth'
|
||||||
|
|
||||||
export const runtime = 'edge'
|
export const runtime = 'edge'
|
||||||
|
|
||||||
|
|
@ -18,8 +17,8 @@ if (!process.env.OPENAI_API_KEY) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const user = await currentUser()
|
const session = await auth()
|
||||||
if (user == null) {
|
if (session == null) {
|
||||||
return new Response('Unauthorized', { status: 401 })
|
return new Response('Unauthorized', { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,7 +39,7 @@ export async function POST(req: Request) {
|
||||||
const stream = OpenAIStream(res, {
|
const stream = OpenAIStream(res, {
|
||||||
async onCompletion(completion) {
|
async onCompletion(completion) {
|
||||||
const title = json.messages[0].content.substring(0, 100)
|
const title = json.messages[0].content.substring(0, 100)
|
||||||
const userId = user.id
|
const userId = session.user.id
|
||||||
const id = json.id ?? nanoid()
|
const id = json.id ?? nanoid()
|
||||||
const createdAt = Date.now()
|
const createdAt = Date.now()
|
||||||
const path = `/chat/${id}`
|
const path = `/chat/${id}`
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { type Metadata } from 'next'
|
import { type Metadata } from 'next'
|
||||||
import { auth } from '@clerk/nextjs'
|
import { auth } from '@/auth'
|
||||||
|
|
||||||
import { Chat } from '@/components/chat'
|
import { Chat } from '@/components/chat'
|
||||||
import { getChat } from '@/app/actions'
|
import { getChat } from '@/app/actions'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
import { ClerkProvider } from '@clerk/nextjs'
|
|
||||||
import { Toaster } from 'react-hot-toast'
|
import { Toaster } from 'react-hot-toast'
|
||||||
|
|
||||||
import '@/app/globals.css'
|
import '@/app/globals.css'
|
||||||
|
|
@ -33,29 +32,25 @@ interface RootLayoutProps {
|
||||||
|
|
||||||
export default function RootLayout({ children }: RootLayoutProps) {
|
export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<ClerkProvider>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<html lang="en" suppressHydrationWarning>
|
<head />
|
||||||
<head />
|
<body
|
||||||
<body
|
className={cn(
|
||||||
className={cn(
|
'font-sans antialiased',
|
||||||
'font-sans antialiased',
|
fontSans.variable,
|
||||||
fontSans.variable,
|
fontMono.variable
|
||||||
fontMono.variable
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<Toaster />
|
||||||
<Toaster />
|
<Providers attribute="class" defaultTheme="system" enableSystem>
|
||||||
<Providers attribute="class" defaultTheme="system" enableSystem>
|
<div className="flex flex-col min-h-screen">
|
||||||
<div className="flex min-h-screen flex-col">
|
{/* @ts-ignore */}
|
||||||
{/* @ts-ignore */}
|
<Header />
|
||||||
<Header />
|
<main className="flex flex-col flex-1 bg-muted/50">{children}</main>
|
||||||
<main className="flex flex-1 flex-col bg-muted/50">
|
</div>
|
||||||
{children}
|
<TailwindIndicator />
|
||||||
</main>
|
</Providers>
|
||||||
</div>
|
</body>
|
||||||
<TailwindIndicator />
|
</html>
|
||||||
</Providers>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
</ClerkProvider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { nanoid } from '@/lib/utils'
|
import { nanoid } from '@/lib/utils'
|
||||||
import { Chat } from '@/components/chat'
|
import { Chat } from '@/components/chat'
|
||||||
|
|
||||||
// export const runtime = 'edge'
|
export const runtime = 'edge'
|
||||||
export const preferredRegion = 'home'
|
|
||||||
|
|
||||||
export default function IndexPage() {
|
export default function IndexPage() {
|
||||||
const id = nanoid()
|
const id = nanoid()
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { ClerkLoaded, ClerkLoading, SignIn } from '@clerk/nextjs'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { ExternalLink } from '@/components/external-link'
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
|
||||||
import { IconSpinner } from '@/components/ui/icons'
|
|
||||||
import { FooterText } from '@/components/footer'
|
|
||||||
|
|
||||||
export default function SignInPage() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full min-h-screen flex-col items-center justify-center">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<ClerkLoading>
|
|
||||||
<div className="flex items-center text-sm text-muted-foreground">
|
|
||||||
<IconSpinner className="mr-2 h-5 w-5" /> Loading...
|
|
||||||
</div>
|
|
||||||
</ClerkLoading>
|
|
||||||
<ClerkLoaded>
|
|
||||||
<SignIn
|
|
||||||
appearance={{
|
|
||||||
elements: {
|
|
||||||
card: 'shadow rounded-lg border border-border',
|
|
||||||
formFieldInput:
|
|
||||||
'flex h-9 w-full rounded-md px-3 py-2 text-sm shadow-sm',
|
|
||||||
formButtonPrimary: 'normal-case',
|
|
||||||
footerAction: 'text-sm',
|
|
||||||
footerActionLink: 'font-medium'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FooterText />
|
|
||||||
</ClerkLoaded>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
13
app/sign-in/page.tsx
Normal file
13
app/sign-in/page.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { FooterText } from '@/components/footer'
|
||||||
|
import { LoginButton } from '@/components/login-button'
|
||||||
|
|
||||||
|
export default function SignInPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full min-h-screen">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<LoginButton />
|
||||||
|
<FooterText />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import { ClerkLoaded, ClerkLoading, SignUp } from '@clerk/nextjs'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
|
||||||
import { ExternalLink } from '@/components/external-link'
|
|
||||||
import { IconSpinner } from '@/components/ui/icons'
|
|
||||||
import { FooterText } from '@/components/footer'
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full min-h-screen flex-col items-center justify-center">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<ClerkLoading>
|
|
||||||
<div className="flex items-center text-sm text-muted-foreground">
|
|
||||||
<IconSpinner className="mr-2 h-5 w-5" /> Loading...
|
|
||||||
</div>
|
|
||||||
</ClerkLoading>
|
|
||||||
<ClerkLoaded>
|
|
||||||
<SignUp
|
|
||||||
appearance={{
|
|
||||||
elements: {
|
|
||||||
card: 'shadow rounded-lg border border-border',
|
|
||||||
formFieldInput:
|
|
||||||
'flex h-9 w-full rounded-md px-3 py-2 text-sm shadow-sm',
|
|
||||||
formButtonPrimary: 'normal-case',
|
|
||||||
footerAction: 'text-sm',
|
|
||||||
footerActionLink: 'font-medium hover:underline',
|
|
||||||
identityPreview: 'rounded-md'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FooterText />
|
|
||||||
</ClerkLoaded>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
30
auth.ts
Normal file
30
auth.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import NextAuth from 'next-auth'
|
||||||
|
import GitHub from 'next-auth/providers/github'
|
||||||
|
|
||||||
|
export const {
|
||||||
|
handlers: { GET, POST },
|
||||||
|
auth,
|
||||||
|
CSRF_experimental
|
||||||
|
// @ts-ignore
|
||||||
|
} = NextAuth({
|
||||||
|
// @ts-ignore
|
||||||
|
providers: [GitHub],
|
||||||
|
callbacks: {
|
||||||
|
// @ts-ignore
|
||||||
|
jwt: async ({ token, profile }) => {
|
||||||
|
if (profile?.id) {
|
||||||
|
token.id = profile.id
|
||||||
|
token.image = profile.picture
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
authorized({ request, auth }) {
|
||||||
|
if (!request.nextUrl.pathname.startsWith('/share/')) return true
|
||||||
|
return !!auth?.user
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: '/sign-in'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -5,25 +5,26 @@ import { buttonVariants } from '@/components/ui/button'
|
||||||
import { Sidebar } from '@/components/sidebar'
|
import { Sidebar } from '@/components/sidebar'
|
||||||
import { SidebarList } from '@/components/sidebar-list'
|
import { SidebarList } from '@/components/sidebar-list'
|
||||||
import { IconGitHub, IconSeparator, IconVercel } from '@/components/ui/icons'
|
import { IconGitHub, IconSeparator, IconVercel } from '@/components/ui/icons'
|
||||||
import { UserButton, currentUser } from '@clerk/nextjs'
|
|
||||||
import { SidebarFooter } from '@/components/sidebar-footer'
|
import { SidebarFooter } from '@/components/sidebar-footer'
|
||||||
import { ThemeToggle } from '@/components/theme-toggle'
|
import { ThemeToggle } from '@/components/theme-toggle'
|
||||||
import { ClearHistory } from '@/components/clear-history'
|
import { ClearHistory } from '@/components/clear-history'
|
||||||
import { clearChats } from '@/app/actions'
|
import { clearChats } from '@/app/actions'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { auth } from '@/auth'
|
||||||
|
import { UserMenu } from './ui/user-menu'
|
||||||
|
import { LoginButton } from './login-button'
|
||||||
|
|
||||||
export async function Header() {
|
export async function Header() {
|
||||||
const user = await currentUser()
|
const session = await auth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 flex h-16 w-full shrink-0 items-center justify-between border-b bg-gradient-to-b from-background/10 via-background/50 to-background/80 px-4 backdrop-blur-xl">
|
<header className="sticky top-0 z-50 flex items-center justify-between w-full h-16 px-4 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{/* @ts-ignore */}
|
{session?.user ? (
|
||||||
{user?.id ? (
|
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<Suspense fallback={<div className="flex-1 overflow-auto" />}>
|
<Suspense fallback={<div className="flex-1 overflow-auto" />}>
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<SidebarList userId={user?.id} />
|
<SidebarList userId={session?.user?.id} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
|
|
@ -32,41 +33,12 @@ export async function Header() {
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
) : (
|
) : (
|
||||||
<Link href="https://vercel.com" target="_blank" rel="nofollow">
|
<Link href="https://vercel.com" target="_blank" rel="nofollow">
|
||||||
<IconVercel className="mr-2 h-6 w-6" />
|
<IconVercel className="w-6 h-6 mr-2" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<IconSeparator className="h-6 w-6 text-muted-foreground/50" />
|
<IconSeparator className="w-6 h-6 text-muted-foreground/50" />
|
||||||
{user?.id ? (
|
{session?.user ? <UserMenu session={session} /> : <LoginButton />}
|
||||||
<UserButton
|
|
||||||
showName
|
|
||||||
appearance={{
|
|
||||||
elements: {
|
|
||||||
afterSignOutUrl: '/',
|
|
||||||
avatarBox: 'w-6 h-6 rounded-full overflow-hidden',
|
|
||||||
userButtonBox: 'flex-row-reverse',
|
|
||||||
userButtonOuterIdentifier: 'text-primary',
|
|
||||||
userButtonPopoverCard:
|
|
||||||
'shadow-lg rounded-lg p-0 border border-border w-[200px] dark:bg-zinc-950 dark:text-zinc-50',
|
|
||||||
userButtonPopoverFooter:
|
|
||||||
'p-4 border-t border-border [&>*]:dark:text-zinc-600',
|
|
||||||
userPreview: 'p-4 border-b border-border m-0',
|
|
||||||
userButtonPopoverActionButton: 'px-1 gap-1',
|
|
||||||
userButtonPopoverActionButtonText:
|
|
||||||
'text-sm tracking-normal dark:text-zinc-400',
|
|
||||||
userButtonPopoverActionButtonIcon:
|
|
||||||
'h-4 w-4 text-muted-foreground'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
href="/sign-in"
|
|
||||||
className={cn(buttonVariants({ variant: 'ghost' }))}
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end space-x-2">
|
<div className="flex items-center justify-end space-x-2">
|
||||||
|
|
@ -77,7 +49,7 @@ export async function Header() {
|
||||||
className={cn(buttonVariants({ variant: 'outline' }))}
|
className={cn(buttonVariants({ variant: 'outline' }))}
|
||||||
>
|
>
|
||||||
<IconGitHub />
|
<IconGitHub />
|
||||||
<span className="ml-2 hidden md:flex">GitHub</span>
|
<span className="hidden ml-2 md:flex">GitHub</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/vercel/nextjs-ai-chatbot/"
|
href="https://github.com/vercel/nextjs-ai-chatbot/"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import * as React from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Button, type ButtonProps } from '@/components/ui/button'
|
import { Button, type ButtonProps } from '@/components/ui/button'
|
||||||
import { IconGitHub, IconSpinner } from '@/components/ui/icons'
|
import { IconGitHub, IconSpinner } from '@/components/ui/icons'
|
||||||
import { useClerk } from '@clerk/nextjs'
|
import { signIn } from 'next-auth/react'
|
||||||
|
|
||||||
interface LoginButtonProps extends ButtonProps {
|
interface LoginButtonProps extends ButtonProps {
|
||||||
text?: string
|
text?: string
|
||||||
|
|
@ -17,14 +17,13 @@ export function LoginButton({
|
||||||
...props
|
...props
|
||||||
}: LoginButtonProps) {
|
}: LoginButtonProps) {
|
||||||
const [isLoading, setIsLoading] = React.useState(false)
|
const [isLoading, setIsLoading] = React.useState(false)
|
||||||
const clerk = useClerk()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
clerk.openSignIn({})
|
signIn('github')
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={cn(className)}
|
className={cn(className)}
|
||||||
|
|
|
||||||
94
components/ui/user-menu.tsx
Normal file
94
components/ui/user-menu.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
// import { type Session } from 'next-auth'
|
||||||
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
|
import { signOut } from 'next-auth/react'
|
||||||
|
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
export interface UserMenuProps {
|
||||||
|
session: any // Session
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserMenu({ session }: UserMenuProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<button className="focus:outline-none">
|
||||||
|
{session.user?.image ? (
|
||||||
|
<Image
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="w-6 h-6 transition-opacity duration-300 rounded-full select-none ring-zinc-100/10 ring-1 hover:opacity-80"
|
||||||
|
src={session.user?.image ? `${session.user.image}&s=60` : ''}
|
||||||
|
alt={session.user.name ?? 'Avatar'}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 p-2 rounded-full select-none shrink-0 bg-gradient-to-r from-cyan-500 to-blue-500">
|
||||||
|
<div
|
||||||
|
className="font-medium uppercase text-zinc-100"
|
||||||
|
style={{ fontSize: 12 }}
|
||||||
|
>
|
||||||
|
{session.user?.name ? session.user?.name.slice(0, 2) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
className="min-w-[200px] bg-white dark:bg-zinc-950 rounded-lg shadow-lg text-zinc-900 dark:text-zinc-400 overflow-hidden border border-transparent dark:border-zinc-700 focus:outline-none relative z-20 py-2"
|
||||||
|
sideOffset={8}
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item className="px-3 py-2 focus:outline-none">
|
||||||
|
<div className="text-xs font-medium">{session.user?.name}</div>
|
||||||
|
<div className="text-xs text-zinc-500">{session.user?.email}</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
<DropdownMenu.Separator className="my-1 h-[1px] bg-zinc-100 dark:bg-zinc-800" />
|
||||||
|
<DropdownMenu.Item className="px-3 py-2 text-xs transition-colors duration-200 cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-800 focus:outline-none ">
|
||||||
|
<a
|
||||||
|
href="https://vercel.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center justify-between w-full"
|
||||||
|
>
|
||||||
|
<span>Vercel Homepage</span>
|
||||||
|
<span>
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height={16}
|
||||||
|
shapeRendering="geometricPrecision"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width={16}
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mr-1"
|
||||||
|
>
|
||||||
|
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
|
||||||
|
<path d="M15 3h6v6" />
|
||||||
|
<path d="M10 14L21 3" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
className="px-3 py-2 text-xs transition-colors duration-200 cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-800 focus:outline-none"
|
||||||
|
onClick={() => signOut()}
|
||||||
|
>
|
||||||
|
Log Out
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
UserMenu.displayName = 'UserMenu'
|
||||||
|
|
@ -1,10 +1 @@
|
||||||
import { authMiddleware } from '@clerk/nextjs'
|
export { auth as middleware } from './auth'
|
||||||
|
|
||||||
// @see https://clerk.dev
|
|
||||||
export default authMiddleware({
|
|
||||||
publicRoutes: ['/share/:id']
|
|
||||||
})
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)']
|
|
||||||
}
|
|
||||||
|
|
|
||||||
13
next-auth.d.ts
vendored
Normal file
13
next-auth.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import NextAuth, { DefaultSession } from 'next-auth'
|
||||||
|
|
||||||
|
declare module 'next-auth' {
|
||||||
|
/**
|
||||||
|
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||||
|
*/
|
||||||
|
interface Session {
|
||||||
|
user: {
|
||||||
|
/** The user's postal address. */
|
||||||
|
address: string
|
||||||
|
} & DefaultSession['user']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,9 +14,9 @@
|
||||||
"format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache"
|
"format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/nextjs": "^4.21.1",
|
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
|
@ -31,7 +31,8 @@
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"focus-trap-react": "^10.1.1",
|
"focus-trap-react": "^10.1.1",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"next": "13.4.6-canary.7",
|
"next": "13.4.7-canary.1",
|
||||||
|
"next-auth": "0.0.0-manual.4cd21ea5",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"openai-edge": "^0.5.1",
|
"openai-edge": "^0.5.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|
@ -55,7 +56,7 @@
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"drizzle-kit": "^0.18.0",
|
"drizzle-kit": "^0.18.0",
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.31.0",
|
||||||
"eslint-config-next": "13.4.6-canary.7",
|
"eslint-config-next": "13.4.7-canary.1",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-tailwindcss": "^3.12.0",
|
"eslint-plugin-tailwindcss": "^3.12.0",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
|
|
@ -67,7 +68,7 @@
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@auth/core": "0.0.0-manual.527fff6c"
|
"@auth/nextjs": "0.0.0-manual.223c6467"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
679
pnpm-lock.yaml
generated
679
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -24,6 +24,12 @@
|
||||||
],
|
],
|
||||||
"strictNullChecks": true
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"next-auth.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue