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

feat: quickstarts: draft for using server components #2225

Merged
merged 28 commits into from Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ae45be9
feat(quickstarts): draft for using server components
onehassan Sep 5, 2023
06c640b
chore: delete unnecessary files
onehassan Sep 5, 2023
9367e91
feat: examples: add other sign in methods
onehassan Sep 9, 2023
218ec31
feat(quickstarts): refactor and organize signup/signin
onehassan Sep 11, 2023
e5fcfb3
refactor: make sure to return refresh token
onehassan Sep 12, 2023
40039fe
Revert "refactor: make sure to return refresh token"
onehassan Sep 13, 2023
6593fdd
fix: make sure refreshToken is returned after signin/signup
onehassan Sep 16, 2023
7225712
chore: update hasura auth
onehassan Sep 16, 2023
42ec665
fix: return refreshToken in getAuthenticationResult
onehassan Sep 16, 2023
a412319
feat: add signin with pat
onehassan Sep 16, 2023
d67fd59
feat: todos CRUD
onehassan Sep 21, 2023
732a4f4
wip
onehassan Sep 26, 2023
12280f7
feat: pat list pagination
onehassan Sep 26, 2023
e0e44b2
fix: set same path for session cookie
onehassan Sep 26, 2023
c8aea78
fix: tweak todo item layout
onehassan Sep 26, 2023
7645975
fix: make sure that hasura-storage-js works on EdgeRuntime
onehassan Sep 26, 2023
049e315
fix: set correct path on cookie on oauth signin
onehassan Sep 26, 2023
4418d6a
chore: cleanup
onehassan Sep 27, 2023
735b779
chore: clean up database setup
onehassan Sep 27, 2023
f5f662a
chore: refactor server actions
onehassan Sep 28, 2023
5077283
chore: merge oauth handling in middleware
onehassan Sep 28, 2023
fdecac9
refactor: add high order component for protected pages
onehassan Sep 28, 2023
d3186ae
refactor: extract session middleware into helper function
onehassan Sep 28, 2023
679b34b
chore: cleanup
onehassan Sep 28, 2023
92c475b
chore: add missing refreshToken
onehassan Sep 28, 2023
4fe4a16
chore: add changeset
onehassan Sep 29, 2023
ebc5913
chore: naming consistency
onehassan Sep 29, 2023
9eb814c
chore: update readme
onehassan Sep 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/beige-maps-heal.md
@@ -0,0 +1,8 @@
---
'@nhost/docs': patch
'@nhost/hasura-auth-js': patch
'@nhost/react': patch
'@nhost/vue': patch
---

return `refreshToken` immediately after signIn and signUp
7 changes: 7 additions & 0 deletions .changeset/clever-roses-attack.md
@@ -0,0 +1,7 @@
---
'@nhost/hasura-storage-js': patch
---

- accept FormData exported from [`form-data`](https://www.npmjs.com/package/form-data) as LegacyFormData
- accept native FormData available on node18 and above
- call native fetch available on node18 and above when running on [EdgeRuntime](https://edge-runtime.vercel.app/)
5 changes: 5 additions & 0 deletions .changeset/early-bulldogs-hear.md
@@ -0,0 +1,5 @@
---
'@nhost-examples/nextjs-server-components': minor
---

new quickstart project that demonstrates how to use the Nhost SDK with Next.js 13 server components
@@ -0,0 +1,2 @@
NEXT_PUBLIC_NHOST_SUBDOMAIN=local
NEXT_PUBLIC_NHOST_REGION=
6 changes: 6 additions & 0 deletions examples/quickstarts/nextjs-server-components/.eslintrc.js
@@ -0,0 +1,6 @@
module.exports = {
extends: ['../../config/.eslintrc.js', 'plugin:@next/next/recommended'],
rules: {
'react/react-in-jsx-scope': 'off'
}
}
35 changes: 35 additions & 0 deletions examples/quickstarts/nextjs-server-components/.gitignore
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
70 changes: 70 additions & 0 deletions examples/quickstarts/nextjs-server-components/README.md
@@ -0,0 +1,70 @@
# Nhost with Next.js Server Components

This quickstart showcases how to correctly add authentication to a Next.js 13 project using the new App Router and Server Components. The other parts of the SDK (Storage / GraphQL/ Functions) should work the same as before.

## Authentication

1. **Saving the auth session**

To enable authentication with Server Components we have to store the auth session in a cookie. This should be done right after any **signIn** or **signUp** operation. See example [here](https://github.com/nhost/nhost/blob/main/examples/quickstarts/nextjs-server-components/src/app/server-actions/auth/sign-in-email-password.ts).

2. **Oauth & refresh session middleware**

Create a middleware at the root of your project that calls the helper method `manageAuthSession`. Feel free to copy paste the the contents of the `/utils` folder to your project. The second argument for `manageAuthSession` is for handling the case where there's an error refreshing the current session with the `refreshToken` stored in the cookie.

```typescript
import { manageAuthSession } from '@utils/nhost'
import { NextRequest, NextResponse } from 'next/server'

export async function middleware(request: NextRequest) {
return manageAuthSession(request, () =>
NextResponse.redirect(new URL('/auth/sign-in', request.url))
)
}
```

3. **Protected routes**

To make sure only authenticated users access some Server Components, wrap them in the Higher Order Server Component `withAuthAsync`.

```typescript
import withAuthAsync from '@utils/auth-guard'

const MyProtectedServerComponent = async () => {
return <h2>Protected</h2>
}

export default withAuthAsync(MyProtectedServerComponent)
```

## Get Started

1. Clone the repository

```sh
git clone https://github.com/nhost/nhost
cd nhost
```

2. Install and build dependencies

```sh
pnpm install
pnpm build
```

3. Terminal 1: Start the Nhost Backend

> Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli).

```sh
cd examples/quickstarts/nhost-backend
nhost up
```

4. Terminal 2: Start the Next.js application

```sh
cd examples/quickstarts/nextjs-server-components
pnpm dev
```
@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true
}
}

module.exports = nextConfig
35 changes: 35 additions & 0 deletions examples/quickstarts/nextjs-server-components/package.json
@@ -0,0 +1,35 @@
{
"name": "@nhost-examples/nextjs-server-components",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@apollo/client": "^3.8.2",
"@nhost/nhost-js": "workspace:^",
"autoprefixer": "10.4.15",
"cookies-next": "^3.0.0",
"eslint": "8.48.0",
"eslint-config-next": "13.4.19",
"form-data": "^4.0.0",
"js-cookie": "^3.0.5",
"next": "13.4.19",
"postcss": "8.4.29",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwind-merge": "^1.8.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2",
"xstate": "^4.38.2"
},
"devDependencies": {
"@types/js-cookie": "^3.0.2",
"@types/node": "20.5.6",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7"
}
}
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,36 @@
'use client'

import Input from '@components/input'
import SubmitButton from '@components/submit-button'
import { signIn } from '@server-actions/auth'
import { useState } from 'react'

export default function SignInWithEmailAndPassword() {
const [error, setError] = useState('')

async function handleSignIn(formData: FormData) {
const response = await signIn(formData)

if (response?.error) {
setError(response.error)
}
}

return (
<div className="flex flex-col items-center">
<h1 className="text-2xl font-semibold text-center">Sign in with email and password</h1>

{error && <p className="mt-3 font-semibold text-center text-red-500">{error}</p>}

<form className="w-full max-w-lg space-y-5" action={handleSignIn}>
<Input label="Email" id="email" name="email" type="email" required />

<Input label="Password" id="password" name="password" type="password" required />

<SubmitButton type="submit" className="w-full">
Sign in
</SubmitButton>
</form>
</div>
)
}
@@ -0,0 +1,57 @@
'use client'

import Input from '@components/input'
import SubmitButton from '@components/submit-button'
import { NhostClient } from '@nhost/nhost-js'
import { useState, type FormEvent } from 'react'

const nhost = new NhostClient({
subdomain: process.env.NEXT_PUBLIC_NHOST_SUBDOMAIN,
region: process.env.NEXT_PUBLIC_NHOST_REGION
})

export default function SignInMagickLink() {
const [email, setEmail] = useState('')
const [error, setError] = useState('')
const [isSuccess, setIsSuccess] = useState(false)

const handleSignIn = async (e: FormEvent) => {
e.preventDefault()

const { error } = await nhost.auth.signIn({ email })

if (error) {
setError(error.message)
} else {
setIsSuccess(true)
}
}

return (
<div className="flex flex-col items-center">
<h1 className="text-2xl font-semibold text-center">Sign in with a magick link</h1>

{error && <p className="mt-3 font-semibold text-center text-red-500">{error}</p>}
{isSuccess && (
<p className="mt-3 font-semibold text-center text-green-500">
Click the link in the email to finish the sign in process
</p>
)}

<form className="w-full max-w-lg space-y-5" onSubmit={handleSignIn}>
<Input
label="Email"
id="email"
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.currentTarget.value)}
required
/>
<SubmitButton type="submit" className="w-full">
Sign In
</SubmitButton>
</form>
</div>
)
}
@@ -0,0 +1,67 @@
'use client'

import { signInWithGoogle } from '@server-actions/auth'
import { useRouter } from 'next/navigation'

export default function SignIn() {
const router = useRouter()

return (
<div className="container flex justify-center">
<div className="w-full max-w-lg space-y-5">
<h1 className="text-2xl font-semibold text-center">Sign In</h1>

<button
className="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
onClick={() => router.push('/auth/sign-in/email-password')}
>
with email/password
</button>

<button
className="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
onClick={() => router.push('/auth/sign-in/webauthn')}
>
with a security key
</button>

<button
className="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
onClick={() => router.push('/auth/sign-in/magick-link')}
>
with a magick link
</button>

<button
className="inline-flex items-center justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-lg shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 focus:outline-none"
onClick={() => router.push('/auth/sign-in/pat')}
>
with a Personal Access Token
</button>

<button
type="button"
className="text-white w-full bg-[#4285F4] hover:bg-[#4285F4]/90 focus:ring-4 focus:outline-none focus:ring-[#4285F4]/50 font-medium rounded-lg px-5 py-2.5 text-center inline-flex items-center justify-between dark:focus:ring-[#4285F4]/55 mr-2 mb-2"
onClick={() => signInWithGoogle()}
>
<svg
className="w-4 h-4 mr-2 -ml-1"
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="google"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 488 512"
>
<path
fill="currentColor"
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
/>
</svg>
with Google <span />
</button>
</div>
</div>
)
}
@@ -0,0 +1,33 @@
'use client'

import Input from '@components/input'
import SubmitButton from '@components/submit-button'
import { signInWithPAT } from '@server-actions/auth'
import { useState } from 'react'

export default function SignInWithPAT() {
const [error, setError] = useState('')

async function handleSignIn(formData: FormData) {
const response = await signInWithPAT(formData)

if (response?.error) {
setError(response.error)
}
}

return (
<div className="flex flex-col items-center gap-4">
<h1 className="text-2xl font-semibold text-center">Sign In with Personal Access Token</h1>

{error && <p className="mt-3 font-semibold text-center text-red-500">{error}</p>}

<form className="w-full max-w-lg space-y-5" action={handleSignIn}>
<Input label="PAT" id="pat" name="pat" required />
<SubmitButton type="submit" className="w-full">
Sign In
</SubmitButton>
</form>
</div>
)
}