Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Next.js server actions and components 1st-class Sanctum citizens #58

Closed
wants to merge 8 commits into from
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
NEXT_PUBLIC_BACKEND_URL=http://localhost:8000

# Nextjs SSR variables
LARAVEL_CSRF_COOKIE_NAME=XSRF-TOKEN
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
* text=auto
* text=auto eol=lf

/.github export-ignore
.editorconfig export-ignore
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ yarn-error.log*

# vercel
.vercel

# IDE
.idea
8 changes: 8 additions & 0 deletions src/actions/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use server'

import { serverFetch } from '@/lib/serverFetch'

export const getUserAction = async () => {

return await serverFetch('/api/user')
}
File renamed without changes.
File renamed without changes.
4 changes: 3 additions & 1 deletion src/app/(app)/Navigation.js → src/app/(app)/Navigation.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import ApplicationLogo from '@/components/ApplicationLogo'
import Dropdown from '@/components/Dropdown'
import Link from 'next/link'
Expand Down Expand Up @@ -154,4 +156,4 @@ const Navigation = ({ user }) => {
)
}

export default Navigation
export default Navigation
14 changes: 14 additions & 0 deletions src/app/(app)/UserRefresh.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client'

import { useAuth } from '@/hooks/auth'

function UserRefresh() {

useAuth({
middleware: 'auth'
})

return <></>
}

export default UserRefresh
File renamed without changes.
12 changes: 7 additions & 5 deletions src/app/(app)/layout.js → src/app/(app)/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use client'

import { useAuth } from '@/hooks/auth'
import Navigation from '@/app/(app)/Navigation'
import Loading from '@/app/(app)/Loading'
import { getUserAction } from '@/actions/actions'
import UserRefresh from '@/app/(app)/UserRefresh'

const AppLayout = async ({ children }) => {

const AppLayout = ({ children }) => {
const { user } = useAuth({ middleware: 'auth' })
const user = await getUserAction();

if (!user) {
return <Loading />
Expand All @@ -16,6 +16,8 @@ const AppLayout = ({ children }) => {
<Navigation user={user} />

<main>{children}</main>

<UserRefresh />
</div>
)
}
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 4 additions & 2 deletions src/hooks/auth.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import useSWR from 'swr'
import axios from '@/lib/axios'
import { useEffect } from 'react'
Expand Down Expand Up @@ -103,9 +105,9 @@ export const useAuth = ({ middleware, redirectIfAuthenticated } = {}) => {
if (middleware === 'guest' && redirectIfAuthenticated && user)
router.push(redirectIfAuthenticated)

if (middleware === 'auth' && !user?.email_verified_at)
if (middleware === 'auth' && user && !user?.email_verified_at)
router.push('/verify-email')

if (
window.location.pathname === '/verify-email' &&
user?.email_verified_at
Expand Down
55 changes: 55 additions & 0 deletions src/lib/serverFetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const DEFAULT_HEADERS = {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-Requested-With': 'XMLHttpRequest',
}

/**
* Forwards cookies from the client to the server if run on the server, adds base URL, and adds additional headers.
*/
export async function serverFetch(route, requestInit = {}) {

const options = {
...requestInit,
credentials: 'include',
cache: 'no-cache',
headers: {
...DEFAULT_HEADERS,
...(await getHeaders()),
...requestInit.headers,
},
}

// Strip leading slash from route.
const normalizedRoute = route.startsWith('/') ? route.slice(1) : route
const url = new URL(normalizedRoute, process.env.NEXT_PUBLIC_BACKEND_URL)

const response = await fetch(url, options)

if (!response.ok) {
throw new Error(`Failed to fetch ${route}. Status: ${response.status}`)
}

return await response.json()
}

const getHeaders = async () => {
// 💡 If running on server, include the headers of the current request.
return typeof window === 'undefined' ? getServerHeaders() : {}
}

const getServerHeaders = async () => {
const { headers, cookies } = await import('next/headers')
const headerStore = await headers()
const Referer = headerStore.get('host')
const cookie = headerStore.get('cookie')

const cookieStore = await cookies()
const csrf = cookieStore.get(process.env.LARAVEL_CSRF_COOKIE_NAME || 'XSRF-TOKEN')?.value

return {
...(Referer && { Referer }),
...(cookie && { cookie }),
...(csrf && { 'X-XSRF-TOKEN': decodeURIComponent(csrf) }),
};
};