Skip to content

Commit

Permalink
chore: rewrite next auth to use app router (#202)
Browse files Browse the repository at this point in the history
* chore: rewrite to use app router and add prisma

* chore: add credential auth
  • Loading branch information
Pagebakers authored Jan 31, 2024
1 parent 0a72761 commit 9566bac
Show file tree
Hide file tree
Showing 20 changed files with 3,743 additions and 153 deletions.
35 changes: 35 additions & 0 deletions examples/next-auth/app/(auth)/login-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client'

import { Stack, Card, CardBody } from '@chakra-ui/react'
import { Auth, useAuth } from '@saas-ui/auth'
import { useRouter } from 'next/navigation'
import React from 'react'

import { SaasUILogo } from '@saas-ui/assets'

export default function LoginPage() {
const { isAuthenticated } = useAuth()
const router = useRouter()

React.useEffect(() => {
if (isAuthenticated) {
router.replace('/')
}
}, [isAuthenticated])

return (
<Stack
height="100vh"
alignItems="center"
justifyContent="center"
spacing="10"
>
<SaasUILogo width="120px" />
<Card width="380px" maxW="container.md">
<CardBody>
<Auth type="password" />
</CardBody>
</Card>
</Stack>
)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use client'

import React from 'react'
import { useRouter } from 'next/navigation'

import { Stack, Card, CardBody } from '@chakra-ui/react'
import { SaasUILogo } from '@saas-ui/assets'
import { Auth, AvailableProviders, useAuth } from '@saas-ui/auth'
import { useRouter } from 'next/router'
import React from 'react'

import { FaGithub } from 'react-icons/fa'
import { Logo } from '../components/Logo'

const providers: AvailableProviders = {
github: {
Expand All @@ -13,7 +16,7 @@ const providers: AvailableProviders = {
},
}

const LoginPage = () => {
export default function LoginPage() {
const { isAuthenticated } = useAuth()
const router = useRouter()

Expand All @@ -30,14 +33,12 @@ const LoginPage = () => {
justifyContent="center"
spacing="10"
>
<Logo width="120px" />
<SaasUILogo width="120px" />
<Card width="380px" maxW="container.md">
<CardBody>
<Auth providers={providers} method="magic" />
<Auth providers={providers} type="magiclink" />
</CardBody>
</Card>
</Stack>
)
}

export default LoginPage
44 changes: 44 additions & 0 deletions examples/next-auth/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client'

import React from 'react'
import { useRouter } from 'next/navigation'

import { Stack, Card, CardBody } from '@chakra-ui/react'
import { SaasUILogo } from '@saas-ui/assets'
import { Auth, AvailableProviders, useAuth } from '@saas-ui/auth'

import { FaGithub } from 'react-icons/fa'

const providers: AvailableProviders = {
github: {
icon: FaGithub,
name: 'Github',
},
}

export default function SignupPage() {
const { isAuthenticated } = useAuth()
const router = useRouter()

React.useEffect(() => {
if (isAuthenticated) {
router.replace('/')
}
}, [isAuthenticated])

return (
<Stack
height="100vh"
alignItems="center"
justifyContent="center"
spacing="10"
>
<SaasUILogo width="120px" />
<Card width="380px" maxW="container.md">
<CardBody>
<Auth providers={providers} type="magiclink" view="signup" />
</CardBody>
</Card>
</Stack>
)
}
6 changes: 6 additions & 0 deletions examples/next-auth/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { authConfig } from '@/lib/auth'
import NextAuth from 'next-auth'

const handler = NextAuth(authConfig)

export { handler as GET, handler as POST }
30 changes: 30 additions & 0 deletions examples/next-auth/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextAuthProvider } from '@/context/AuthProvider'
import { SaasProvider } from '@saas-ui/react'
import { authConfig } from '@/lib/auth'
import { getServerSession } from 'next-auth'
import { ColorModeScript } from '@chakra-ui/react'
import { cookies } from 'next/headers'

export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const cookieStore = cookies()

const colorMode = (cookieStore.get('chakra-ui-color-mode')?.value ??
'dark') as 'light' | 'dark'

const session = await getServerSession(authConfig)

return (
<html lang="en" data-theme={colorMode} style={{ colorScheme: colorMode }}>
<body className={`chakra-ui-${colorMode}`}>
<SaasProvider>
<ColorModeScript initialColorMode={colorMode} type="cookie" />
<NextAuthProvider session={session}>{children}</NextAuthProvider>
</SaasProvider>
</body>
</html>
)
}
18 changes: 18 additions & 0 deletions examples/next-auth/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client'

import { Authenticated } from '@/components/Authenticated'
import { Box, Button } from '@chakra-ui/react'
import { useAuth } from '@saas-ui/auth'

export default function HomePage() {
const { user, logOut } = useAuth()
console.log('user', user)
return (
<Authenticated>
<Box>
Logged in as: {user?.email}.{' '}
<Button onClick={() => logOut()}>Log out</Button>
</Box>
</Authenticated>
)
}
19 changes: 19 additions & 0 deletions examples/next-auth/components/Authenticated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import React from 'react'

export const Authenticated: React.FC<React.PropsWithChildren> = (props) => {
const router = useRouter()
const { data } = useSession({
required: true,
onUnauthenticated() {
router.replace('/login')
},
})

if (!data) {
return null
}

return props.children
}
121 changes: 103 additions & 18 deletions examples/next-auth/context/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,99 @@
'use client'

import { AuthProvider, AuthProviderProps, User } from '@saas-ui/auth'
import { SessionProvider, signIn, signOut, useSession } from 'next-auth/react'
import {
SessionProvider,
signIn,
signOut,
useSession,
getSession,
} from 'next-auth/react'
import { Session } from 'next-auth'
import React from 'react'
import { LoadingOverlay, LoadingSpinner } from '@saas-ui/react'
import { BroadcastChannel } from 'next-auth/client/_utils'

const createAuthService = (session: Session | null): AuthProviderProps => {
return {
onLogin: async ({ provider, email }, options) => {
const type = provider ?? 'email'
const createAuthService = ({
getUser,
}: {
getUser: () => User | null
}): AuthProviderProps => {
const channel = BroadcastChannel()

let user: User | null = getUser()

return {
onAuthStateChange(callback) {
// we need to use a broadcast channel here to make sure that the
// auth state is updated across all tabs
return channel.receive(async (message) => {
if (
message.event === 'session' &&
message.data?.trigger === 'getSession'
) {
const session = await getSession()
callback((session?.user as User) ?? null)
}
})
},
onLogin: async ({ provider, email, password }, options) => {
let type = 'email'
let params: Record<string, any> = {
callbackUrl: options?.redirectTo,
}

if (email) {
if (provider) {
type = provider
} else if (email && password) {
type = 'credentials'
params = {
email,
password,
redirect: false,
...params,
}
} else if (email) {
params = {
email,
redirect: false, // do not redirect to NextAuth login page
...params,
}
} else {
throw new Error('Unknown login method')
}

const result = await signIn(type, params)

if (result?.ok) {
//
if (result && !result?.ok) {
throw new Error(result?.error ?? 'Unknown error')
}

if (type === 'credentials') {
// result doesn't return the user data, so fetch the session here.
// this will fetch the session twice though.
const session = await getSession()
user = session?.user as User
return user
}

return undefined
},
onLogout: () => signOut(),
onLoadUser: async () => (session?.user as User) || null,
onLogout: async () => {
await signOut({
redirect: false,
})
user = null
},
onLoadUser: async () => {
user = getUser()
return user
},
onGetToken: async () => {
// we don't have access to any token, so just returning the user email here.
//
return session?.user?.email
// we don't have access to the token here, so returning the user email instead.
if (!user) {
user = getUser()
}
return user?.email
},
}
}
Expand All @@ -42,10 +102,14 @@ export interface NextAuthProviderProps {
/**
* Optionally get this with getInitialProps or getServerSideProps
*/
session: Session
session: Session | null
children: React.ReactNode
}

/**
* Next Auth Provider for Saas UI
* Supports oauth providers and magic link login.
*/
export const NextAuthProvider: React.FC<NextAuthProviderProps> = (props) => {
const { children, session } = props
return (
Expand All @@ -59,8 +123,29 @@ export const NextAuthProvider: React.FC<NextAuthProviderProps> = (props) => {
* Wrap the AuthProvider here so we can get access to the NextAuth context
*/
const Provider: React.FC<React.PropsWithChildren> = (props) => {
const { data } = useSession()
return (
<AuthProvider {...createAuthService(data)}>{props.children}</AuthProvider>
)
const { data, status } = useSession()

const authService = React.useMemo(() => {
// when data is undefined the session is still loading
if (typeof data === 'undefined') {
return
}

return createAuthService({
getUser() {
return data?.user ?? null
},
})
}, [data, status])

// if we don't have access to the session yet, show a loading spinner
if (!authService) {
return (
<LoadingOverlay variant="fullscreen">
<LoadingSpinner />
</LoadingOverlay>
)
}

return <AuthProvider {...authService}>{props.children}</AuthProvider>
}
13 changes: 13 additions & 0 deletions examples/next-auth/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
postgres:
image: postgres:16
restart: unless-stopped
# Uncomment the following line to enable query logging
# Then restart the container.
# command: ['postgres', '-c', 'log_statement=all']
environment:
POSTGRES_DB: postgres
POSTGRES_USER: nextauth
POSTGRES_PASSWORD: changeme
ports:
- '5432:5432'
Loading

0 comments on commit 9566bac

Please sign in to comment.