diff --git a/examples/with-nhost-auth-realtime-graphql/.env.local.example b/examples/with-nhost-auth-realtime-graphql/.env.local.example new file mode 100644 index 0000000000..38a91f151a --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/.env.local.example @@ -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 \ No newline at end of file diff --git a/examples/with-nhost-auth-realtime-graphql/.gitignore b/examples/with-nhost-auth-realtime-graphql/.gitignore new file mode 100644 index 0000000000..1437c53f70 --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/.gitignore @@ -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 diff --git a/examples/with-nhost-auth-realtime-graphql/README.md b/examples/with-nhost-auth-realtime-graphql/README.md new file mode 100644 index 0000000000..068f94e48a --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/README.md @@ -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)). diff --git a/examples/with-nhost-auth-realtime-graphql/components/private-route.js b/examples/with-nhost-auth-realtime-graphql/components/private-route.js new file mode 100644 index 0000000000..ae7d89ac67 --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/components/private-route.js @@ -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
Checking auth...
+ } + + if (!signedIn) { + router.push('/login') + return
Redirecting...
+ } + + return + } +} diff --git a/examples/with-nhost-auth-realtime-graphql/package.json b/examples/with-nhost-auth-realtime-graphql/package.json new file mode 100644 index 0000000000..266970d60b --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/package.json @@ -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" + } +} diff --git a/examples/with-nhost-auth-realtime-graphql/pages/_app.js b/examples/with-nhost-auth-realtime-graphql/pages/_app.js new file mode 100644 index 0000000000..a6551a3803 --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/pages/_app.js @@ -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 ( + + + + + + ) +} + +export default MyApp diff --git a/examples/with-nhost-auth-realtime-graphql/pages/api/hello.js b/examples/with-nhost-auth-realtime-graphql/pages/api/hello.js new file mode 100644 index 0000000000..8f603094bd --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/pages/api/hello.js @@ -0,0 +1,3 @@ +export default function handler(req, res) { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/examples/with-nhost-auth-realtime-graphql/pages/index.js b/examples/with-nhost-auth-realtime-graphql/pages/index.js new file mode 100644 index 0000000000..ae8e9b19ed --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/pages/index.js @@ -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 ( +
+
+
+ setName(e.target.value)} + /> +
+
+ +
+
+
+ ) +} + +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
Loading...
+ } + + if (error) { + return
Error loading items
+ } + + const { items } = data + + return ( +
+ {items.map((item) => { + return ( +
  • + {item.name} [ + handleDeleteItem(item.id)}>delete] +
  • + ) + })} +
    + ) +} + +function Home() { + return ( +
    +
    +
    {nhost.auth.user().display_name}
    +
    + +
    +
    +

    Dashboard

    + + +
    + ) +} + +export default PrivateRoute(Home) diff --git a/examples/with-nhost-auth-realtime-graphql/pages/login.js b/examples/with-nhost-auth-realtime-graphql/pages/login.js new file mode 100644 index 0000000000..838e123501 --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/pages/login.js @@ -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 ( +
    +
    +
    + setEmail(e.target.value)} + /> +
    +
    + setPassword(e.target.value)} + /> +
    +
    + +
    +
    + +
    + Register +
    +
    + ) +} diff --git a/examples/with-nhost-auth-realtime-graphql/pages/register.js b/examples/with-nhost-auth-realtime-graphql/pages/register.js new file mode 100644 index 0000000000..62475f83a6 --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/pages/register.js @@ -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 ( +
    +
    +
    + setDisplayName(e.target.value)} + /> +
    +
    + setEmail(e.target.value)} + /> +
    +
    + setPassword(e.target.value)} + /> +
    +
    + +
    +
    + +
    + Login +
    +
    + ) +} diff --git a/examples/with-nhost-auth-realtime-graphql/setup/data.sql b/examples/with-nhost-auth-realtime-graphql/setup/data.sql new file mode 100644 index 0000000000..debfb11072 --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/setup/data.sql @@ -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; \ No newline at end of file diff --git a/examples/with-nhost-auth-realtime-graphql/setup/hasura-metadata.json b/examples/with-nhost-auth-realtime-graphql/setup/hasura-metadata.json new file mode 100644 index 0000000000..36b8c40a9a --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/setup/hasura-metadata.json @@ -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" + } + } + } + } + ] + } + ] +} diff --git a/examples/with-nhost-auth-realtime-graphql/utils/nhost.js b/examples/with-nhost-auth-realtime-graphql/utils/nhost.js new file mode 100644 index 0000000000..d661a52c57 --- /dev/null +++ b/examples/with-nhost-auth-realtime-graphql/utils/nhost.js @@ -0,0 +1,5 @@ +import { createClient } from 'nhost-js-sdk' + +export const nhost = createClient({ + baseURL: process.env.NEXT_PUBLIC_BACKEND_URL, +})