Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.

Commit

Permalink
added nextjs, hasura templates. updated react template to include env…
Browse files Browse the repository at this point in the history
… file
  • Loading branch information
hcote committed Feb 17, 2021
1 parent 3881561 commit 433c563
Show file tree
Hide file tree
Showing 29 changed files with 712 additions and 3 deletions.
2 changes: 1 addition & 1 deletion scaffolds/binance-smart-chain/scaffold.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default createScaffold<BinanceSmartChainData>(

{
shortDescription: 'Binance Smart Chain',
order: 1,
order: 3,
installDependenciesCommand: NpmClientPrompt.getInstallCommand,
startCommand: NpmClientPrompt.getStartCommand,
flags: {
Expand Down
86 changes: 86 additions & 0 deletions scaffolds/hasura/scaffold.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import { Template, Zombi } from 'compiled/zombi';
import { createScaffold } from 'cli/utils/scaffold-helpers';
import { mergePrompts } from 'cli/utils/merge-prompts';
import { NpmClientPrompt, PublishableApiKeyPrompt, SecretApiKeyPrompt } from 'scaffolds/prompts';
import type { Questions } from 'compiled/zombi';
import type { Flags } from 'cli/flags';

// prompt specific to the Hasura template, therefore not in `scaffolds/prompts.ts`
namespace HasuraUrlPrompt {
export type Data = {
hasuraUrl: 'npm' | 'yarn';
};

export const questions: Questions<Data> = {
type: 'input',
name: 'hasuraUrl',
message: "Enter your project's Hasura URL:",
};

export const flags: Flags<Data> = {
hasuraUrl: {
type: String,
description: 'The GraphQL URL for your Hasura app.',
},
};
}

// prompt specific to the Hasura template, therefore not in `scaffolds/prompts.ts`
namespace JwtSecretPrompt {
export type Data = {
jwtSecret: 'npm' | 'yarn';
};

export const questions: Questions<Data> = {
type: 'input',
name: 'jwtSecret',
message: 'Enter your 32+ character JWT secret:',
};

export const flags: Flags<Data> = {
jwtSecret: {
type: String,
description: 'The shared JWT secret between your app and Hasura.',
},
};
}

type HasuraData = NpmClientPrompt.Data &
PublishableApiKeyPrompt.Data &
SecretApiKeyPrompt.Data &
HasuraUrlPrompt.Data &
JwtSecretPrompt.Data;

export default createScaffold<HasuraData>(
(props) => (
<Zombi
{...props}
prompts={mergePrompts(
PublishableApiKeyPrompt.questions,
SecretApiKeyPrompt.questions,
HasuraUrlPrompt.questions,
JwtSecretPrompt.questions,
NpmClientPrompt.questions,
)}
>
<Template source="./" />
</Zombi>
),

{
shortDescription: 'Hasura',
order: 2,
installDependenciesCommand: NpmClientPrompt.getInstallCommand,
startCommand: (data: HasuraData) => {
return data.npmClient === 'npm' ? 'npm run dev' : 'yarn dev';
},
flags: {
...NpmClientPrompt.flags,
...PublishableApiKeyPrompt.flags,
...SecretApiKeyPrompt.flags,
...HasuraUrlPrompt.flags,
...JwtSecretPrompt.flags,
},
},
);
4 changes: 4 additions & 0 deletions scaffolds/hasura/template/.env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY=<%= publishableApiKey %>
MAGIC_SECRET_KEY=<%= secretApiKey %>
HASURA_URL=<%= hasuraUrl %>
JWT_SECRET=<%= jwtSecret %>
16 changes: 16 additions & 0 deletions scaffolds/hasura/template/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
**Note one**: This template assumes you have created a table in your Hasura console called `users` with two columns, `issuer` (primary key) and `email`.

**Note two**: the `JWT_SECRET` variable in `.env.local` should match the `key` value set in your `HASURA_GRAPHQL_JWT_SECRET` env variable. This is so Hasura can verify the JWT token sent inthe Authorization header when querying the database. You can set this in your Hasura project dashboard under "Env Vars". Hasura will throw an error if the `key` is less than 32 characters.

#### For example

HASURA_GRAPHQL_JWT_SECRET:

```
{
"key": "abcdefghijklmnopqrstuvwxyz1234567890",
"type": "HS256"
}
```

In `.env.local`: JWT_SECRET=abcdefghijklmnopqrstuvwxyz1234567890
7 changes: 7 additions & 0 deletions scaffolds/hasura/template/components/loading.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className='container'>
<p>Loading...</p>
</div>
);
}
5 changes: 5 additions & 0 deletions scaffolds/hasura/template/magic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Magic } from 'magic-sdk';

const createMagic = (key) => typeof window != 'undefined' && new Magic(key);

export const magic = createMagic(process.env.NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY);
19 changes: 19 additions & 0 deletions scaffolds/hasura/template/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "next-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@magic-sdk/admin": "^1.3.0",
"cookie": "^0.4.1",
"jsonwebtoken": "^8.5.1",
"magic-sdk": "^4.1.1",
"next": "10.0.6",
"react": "17.0.1",
"react-dom": "17.0.1"
}
}
15 changes: 15 additions & 0 deletions scaffolds/hasura/template/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import '../style.css';
import Head from 'next/head';

function MyApp({ Component, pageProps }) {
return (
<>
<Head>
<title>Magic Hasura</title>
</Head>
<Component {...pageProps} />
</>
);
}

export default MyApp;
91 changes: 91 additions & 0 deletions scaffolds/hasura/template/pages/api/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Magic } from '@magic-sdk/admin';
import jwt from 'jsonwebtoken';

const magic = new Magic(process.env.MAGIC_SECRET_KEY);

export default async function login(req, res) {
try {
// Grab auth token from Authorization header
const didToken = req.headers.authorization.substr(7);

// Validate auth token with Magic
await magic.token.validate(didToken);

// Grab user data from auth token
const { email, issuer } = await magic.users.getMetadataByToken(didToken);

// Create a JWT, including user data which we use when saving new user in Hasura
const token = jwt.sign(
{
email,
issuer,
'https://hasura.io/jwt/claims': {
'x-hasura-allowed-roles': ['admin'],
'x-hasura-default-role': 'admin',
'x-hasura-user-id': `${issuer}`,
},
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // one week
},
process.env.JWT_SECRET
);

// Check if user trying to log in already exists
const newUser = await isNewUser(issuer, token);

// If not, create a new user in Hasura
newUser && (await createNewUser(issuer, email, token));

res.status(200).json({ authenticated: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
}

async function isNewUser(issuer, token) {
let query = {
query: `{
users( where: {issuer: {_eq: "${issuer}"}}) {
email
}
}`,
};
try {
let data = await queryHasura(query, token);
return data?.users.length ? false : true;
} catch (error) {
console.log(error);
}
}

async function createNewUser(issuer, email, token) {
let query = {
query: `mutation {
insert_users_one( object: { issuer: "${issuer}", email: "${email}" }) {
email
}
}`,
};
try {
await queryHasura(query, token);
} catch (error) {
console.log(error);
}
}

async function queryHasura(query, token) {
try {
let res = await fetch(process.env.HASURA_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: 'Bearer ' + token,
},
body: JSON.stringify(query),
});
let { data } = await res.json();
return data;
} catch (error) {
console.log(error);
}
}
22 changes: 22 additions & 0 deletions scaffolds/hasura/template/pages/callback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect } from 'react';
import Router from 'next/router';
import Loading from '../components/loading';
import { magic } from '../magic';

export default function Callback() {
useEffect(() => {
// On mount, we try to login with a Magic credential in the URL query.
magic.auth.loginWithCredential().then(async (didToken) => {
// Validate auth token with server
const res = await fetch('/api/login', {
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + didToken,
},
});
res.status === 200 && Router.push('/');
});
}, []);

return <Loading />;
}
39 changes: 39 additions & 0 deletions scaffolds/hasura/template/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useEffect, useState, useCallback } from 'react';
import Router from 'next/router';
import { magic } from '../magic';
import Loading from '../components/loading';

export default function Index() {
const [userMetadata, setUserMetadata] = useState();

useEffect(() => {
// On mount, we check if a user is logged in.
// If so, we'll retrieve the authenticated user's profile.
magic.user.isLoggedIn().then((magicIsLoggedIn) => {
if (magicIsLoggedIn) {
magic.user.getMetadata().then(setUserMetadata);
} else {
// If no user is logged in, redirect to `/login`
Router.push('/login');
}
});
}, []);

/**
* Perform logout action via Magic.
*/
const logout = useCallback(() => {
magic.user.logout().then(() => {
Router.push('/login');
});
}, [Router]);

return userMetadata ? (
<div className='container'>
<h1>Current user: {userMetadata.email}</h1>
<button onClick={logout}>Logout</button>
</div>
) : (
<Loading />
);
}
59 changes: 59 additions & 0 deletions scaffolds/hasura/template/pages/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useCallback, useState } from 'react';
import Router from 'next/router';
import { magic } from '../magic';

export default function Login() {
const [email, setEmail] = useState('');
const [isLoggingIn, setIsLoggingIn] = useState(false);

/**
* Perform login action via Magic's passwordless flow. Upon successuful
* completion of the login flow, a user is redirected to the homepage.
*/
const login = useCallback(async () => {
setIsLoggingIn(true);

try {
// Grab auth token from loginWithMagicLink
const didToken = await magic.auth.loginWithMagicLink({
email,
redirectURI: new URL('/callback', window.location.origin).href,
});

// Validate auth token with server
const res = await fetch('/api/login', {
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + didToken,
},
});
res.status === 200 && Router.push('/');
} catch {
setIsLoggingIn(false);
}
}, [email]);

/**
* Saves the value of our email input into component state.
*/
const handleInputOnChange = useCallback((event) => {
setEmail(event.target.value);
}, []);

return (
<div className='container'>
<h1>Please sign up or login</h1>
<input
type='email'
name='email'
required='required'
placeholder='Enter your email'
onChange={handleInputOnChange}
disabled={isLoggingIn}
/>
<button onClick={login} disabled={isLoggingIn}>
Send
</button>
</div>
);
}
Binary file added scaffolds/hasura/template/public/favicon.ico
Binary file not shown.
Loading

0 comments on commit 433c563

Please sign in to comment.