example: Nhost with auth and realtime graphql example added (#22493)

This commit is contained in:
Johan Eliasson 2021-02-24 15:18:05 +01:00 committed by GitHub
parent 460c7c7ddc
commit a78e904fc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 745 additions and 0 deletions

View file

@ -0,0 +1,2 @@
NEXT_PUBLIC_GRAPHQL_URL=https://hasura-[project-id].nhost.app/v1/graphql
NEXT_PUBLIC_BACKEND_URL=https://backend-[project-id].nhost.app

View file

@ -0,0 +1,34 @@
# 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
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel

View file

@ -0,0 +1,61 @@
# Auth & Realtime GraphQL Example Using Next.js and Nhost
This example showcases Next.js as the frontend using [Nhost](https://nhost.io/) as the backend.
## Demo
### [https://nhost-nextjs-example.vercel.app/](https://nhost-nextjs-example.vercel.app/)
## Deploy Your Own
Once you have created a Nhost project and have access to [the environment variables you'll need](#step-4-add-environment-variables), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-nhost-auth-realtime-graphql&project-name=with-nhost-auth-realtime-graphql&repository-name=with-nhost-auth-realtime-graphql&env=NEXT_PUBLIC_GRAPHQL_URL,NEXT_PUBLIC_BACKEND_URL&envDescription=Enter%20your%20Nhost%20project%27s%20URLs)
## How to use
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
```bash
npx create-next-app --example nhost nhost-app
# or
yarn create next-app --example nhost nhost-app
```
## Configuration
### Step 1. Create an account and a project on Nhost
[Create an account and project on Nhost](https://console.nhost.io).
### Step 2. Create `items` database
Go to your project's Hasura console. Go to the **DATA** tab in the top menu and click **SQL** in the bottom left menu.
Then copy the content from `setup/data.sql` in this example and paste it in the **Raw SQL** form in the Hasura Console. Make sure **Track this** is checked and click **Run!**
### Step 3. Add API metadata
Again, in the Hasura console, click on the **gearwheel** (settings) in the top right menu. Click on **Import metadata** and select the file `setup/hasura-metadata.json` in this repository.
### Step 4. Add environment variables
Copy `.env.local.example` to `.env.local` and update the two URLs with your Nhost project URLs. You find the URLs in the Nhost console dashboard of your project.
### Step 5. Run Next.js in development mode
```bash
npm install
npm run dev
# or
yarn install
yarn dev
```
Your app should be up and running on [http://localhost:3000](http://localhost:3000)! If it doesn't work, post on [GitHub discussions](https://github.com/vercel/next.js/discussions).
### Step 6. Deploy on Vercel
You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).

View file

@ -0,0 +1,22 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useRouter } from 'next/router'
import { useAuth } from '@nhost/react-auth'
export function PrivateRoute(Component) {
return (props) => {
const router = useRouter()
const { signedIn } = useAuth()
// wait to see if the user is logged in or not.
if (signedIn === null) {
return <div>Checking auth...</div>
}
if (!signedIn) {
router.push('/login')
return <div>Redirecting...</div>
}
return <Component {...props} />
}
}

View file

@ -0,0 +1,20 @@
{
"name": "with-nhost-auth-realtime-graphql",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@apollo/client": "3.3.11",
"@nhost/react-apollo": "1.0.7",
"@nhost/react-auth": "1.0.5",
"graphql": "15.5.0",
"next": "10.0.7",
"nhost-js-sdk": "3.0.0-16",
"react": "17.0.1",
"react-dom": "17.0.1"
}
}

View file

@ -0,0 +1,19 @@
import { NhostAuthProvider } from '@nhost/react-auth'
import { NhostApolloProvider } from '@nhost/react-apollo'
import { nhost } from '../utils/nhost'
function MyApp({ Component, pageProps }) {
return (
<NhostAuthProvider auth={nhost.auth}>
<NhostApolloProvider
auth={nhost.auth}
gqlEndpoint={process.env.NEXT_PUBLIC_GRAPHQL_URL}
>
<Component {...pageProps} />
</NhostApolloProvider>
</NhostAuthProvider>
)
}
export default MyApp

View file

@ -0,0 +1,3 @@
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}

View file

@ -0,0 +1,128 @@
import { useState } from 'react'
import { useSubscription, useMutation, gql } from '@apollo/client'
import { PrivateRoute } from '../components/private-route'
import { nhost } from '../utils/nhost'
const INSERT_ITEM = gql`
mutation insertItem($item: items_insert_input!) {
insert_items_one(object: $item) {
id
}
}
`
const S_GET_ITEMS = gql`
subscription sGetItems {
items {
id
name
}
}
`
const DELETE_ITEM = gql`
mutation deleteItem($item_id: uuid!) {
delete_items_by_pk(id: $item_id) {
id
}
}
`
function InsertItem() {
const [name, setName] = useState('')
const [insertItem] = useMutation(INSERT_ITEM)
async function handleFormSubmit(e) {
e.preventDefault()
try {
insertItem({
variables: {
item: {
name,
},
},
})
} catch (error) {
console.error(error)
return alert('Error inserting item')
}
setName('')
}
return (
<div style={{ padding: '10px' }}>
<form onSubmit={handleFormSubmit}>
<div>
<input
type="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<button type="submit">Insert item</button>
</div>
</form>
</div>
)
}
function ListItems() {
const { loading, error, data } = useSubscription(S_GET_ITEMS)
const [deleteItem] = useMutation(DELETE_ITEM)
async function handleDeleteItem(itemId) {
try {
deleteItem({
variables: {
item_id: itemId,
},
})
} catch (error) {
console.log(error)
return alert('Error deleting item')
}
}
if (loading && !data) {
return <div>Loading...</div>
}
if (error) {
return <div>Error loading items</div>
}
const { items } = data
return (
<div style={{ padding: '10px' }}>
{items.map((item) => {
return (
<li key={item.id}>
{item.name} [
<span onClick={() => handleDeleteItem(item.id)}>delete</span>]
</li>
)
})}
</div>
)
}
function Home() {
return (
<div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<pre>{nhost.auth.user().display_name}</pre>
<div style={{ paddingLeft: '10px' }}>
<button onClick={() => nhost.auth.logout()}>logout</button>
</div>
</div>
<h1>Dashboard</h1>
<InsertItem />
<ListItems />
</div>
)
}
export default PrivateRoute(Home)

View file

@ -0,0 +1,54 @@
import { useState } from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import { nhost } from '../utils/nhost'
export default function Login() {
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
async function handleLogin(e) {
e.preventDefault()
try {
await nhost.auth.login({ email, password })
} catch (error) {
console.error(error)
return alert('failed to login')
}
router.push('/')
}
return (
<div>
<form onSubmit={handleLogin}>
<div>
<input
placeholder="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<input
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
<div>
<Link href="/register">Register</Link>
</div>
</div>
)
}

View file

@ -0,0 +1,71 @@
import { useState } from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import { nhost } from '../utils/nhost'
export default function Login() {
const router = useRouter()
const [displayName, setDisplayName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
async function handleLogin(e) {
e.preventDefault()
try {
await nhost.auth.register({
email,
password,
options: {
userData: {
display_name: displayName,
},
},
})
} catch (error) {
console.error(error)
return alert('failed to register')
}
router.push('/')
}
return (
<div>
<form onSubmit={handleLogin}>
<div>
<input
placeholder="Name"
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
/>
</div>
<div>
<input
placeholder="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<input
placeholder="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
<div>
<Link href="/login">Login</Link>
</div>
</div>
)
}

View file

@ -0,0 +1,16 @@
CREATE TABLE public.items (
id uuid DEFAULT public.gen_random_uuid() NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
name text NOT NULL,
user_id uuid NOT NULL
);
ALTER TABLE
ONLY public.items
ADD
CONSTRAINT items_pkey PRIMARY KEY (id);
ALTER TABLE
ONLY public.items
ADD
CONSTRAINT items_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE;

View file

@ -0,0 +1,310 @@
{
"version": 2,
"tables": [
{
"table": {
"schema": "auth",
"name": "account_providers"
},
"object_relationships": [
{
"name": "account",
"using": {
"foreign_key_constraint_on": "account_id"
}
},
{
"name": "provider",
"using": {
"foreign_key_constraint_on": "auth_provider"
}
}
]
},
{
"table": {
"schema": "auth",
"name": "account_roles"
},
"object_relationships": [
{
"name": "account",
"using": {
"foreign_key_constraint_on": "account_id"
}
},
{
"name": "roleByRole",
"using": {
"foreign_key_constraint_on": "role"
}
}
]
},
{
"table": {
"schema": "auth",
"name": "accounts"
},
"object_relationships": [
{
"name": "role",
"using": {
"foreign_key_constraint_on": "default_role"
}
},
{
"name": "user",
"using": {
"foreign_key_constraint_on": "user_id"
}
}
],
"array_relationships": [
{
"name": "account_providers",
"using": {
"foreign_key_constraint_on": {
"column": "account_id",
"table": {
"schema": "auth",
"name": "account_providers"
}
}
}
},
{
"name": "account_roles",
"using": {
"foreign_key_constraint_on": {
"column": "account_id",
"table": {
"schema": "auth",
"name": "account_roles"
}
}
}
},
{
"name": "refresh_tokens",
"using": {
"foreign_key_constraint_on": {
"column": "account_id",
"table": {
"schema": "auth",
"name": "refresh_tokens"
}
}
}
}
],
"select_permissions": [
{
"role": "me",
"permission": {
"columns": ["email", "id"],
"filter": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
}
}
]
},
{
"table": {
"schema": "auth",
"name": "providers"
},
"array_relationships": [
{
"name": "account_providers",
"using": {
"foreign_key_constraint_on": {
"column": "auth_provider",
"table": {
"schema": "auth",
"name": "account_providers"
}
}
}
}
]
},
{
"table": {
"schema": "auth",
"name": "refresh_tokens"
},
"object_relationships": [
{
"name": "account",
"using": {
"foreign_key_constraint_on": "account_id"
}
}
]
},
{
"table": {
"schema": "auth",
"name": "roles"
},
"array_relationships": [
{
"name": "account_roles",
"using": {
"foreign_key_constraint_on": {
"column": "role",
"table": {
"schema": "auth",
"name": "account_roles"
}
}
}
},
{
"name": "accounts",
"using": {
"foreign_key_constraint_on": {
"column": "default_role",
"table": {
"schema": "auth",
"name": "accounts"
}
}
}
}
]
},
{
"table": {
"schema": "public",
"name": "items"
},
"object_relationships": [
{
"name": "user",
"using": {
"foreign_key_constraint_on": "user_id"
}
}
],
"insert_permissions": [
{
"role": "user",
"permission": {
"check": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
},
"set": {
"user_id": "x-hasura-user-id"
},
"columns": ["name"],
"backend_only": false
}
}
],
"select_permissions": [
{
"role": "user",
"permission": {
"columns": ["id", "created_at", "name", "user_id"],
"filter": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
}
}
],
"delete_permissions": [
{
"role": "user",
"permission": {
"filter": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
}
}
]
},
{
"table": {
"schema": "public",
"name": "users"
},
"object_relationships": [
{
"name": "account",
"using": {
"manual_configuration": {
"remote_table": {
"schema": "auth",
"name": "accounts"
},
"column_mapping": {
"id": "user_id"
}
}
}
}
],
"array_relationships": [
{
"name": "items",
"using": {
"foreign_key_constraint_on": {
"column": "user_id",
"table": {
"schema": "public",
"name": "items"
}
}
}
}
],
"select_permissions": [
{
"role": "me",
"permission": {
"columns": [
"id",
"created_at",
"updated_at",
"display_name",
"avatar_url"
],
"filter": {
"id": {
"_eq": "X-Hasura-User-Id"
}
}
}
},
{
"role": "user",
"permission": {
"columns": [
"id",
"created_at",
"updated_at",
"display_name",
"avatar_url"
],
"filter": {
"id": {
"_eq": "X-Hasura-User-Id"
}
}
}
}
]
}
]
}

View file

@ -0,0 +1,5 @@
import { createClient } from 'nhost-js-sdk'
export const nhost = createClient({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
})