This repository has been archived by the owner on Aug 16, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
added nextjs, hasura templates. updated react template to include env…
… file
- Loading branch information
Showing
29 changed files
with
712 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"}}) { | ||
} | ||
}`, | ||
}; | ||
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}" }) { | ||
} | ||
}`, | ||
}; | ||
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 /> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Oops, something went wrong.