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

Example 5 - With Fauna GQL Upload (FGU) #19

Draft
wants to merge 43 commits into
base: with-faunadb-auth
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7625102
With FaunaDB Authentication (#18)
Vadorequest Mar 18, 2021
dc331e3
Add overview doc
Vadorequest Mar 18, 2021
5d79676
Move overview
Vadorequest Mar 18, 2021
268e728
Fix DeleteIfExists
Vadorequest Mar 18, 2021
16b55c8
Misc
Vadorequest Mar 18, 2021
d0d714c
Add FGU with basic files from their example (WIP)
Vadorequest Mar 19, 2021
434ee58
Fix membership definition for roles
Vadorequest Mar 19, 2021
d425f62
Apply our roles/index instead of the ones from the demo
Vadorequest Mar 19, 2021
f33603c
Fix roles GQL (missing "Query" wrapper)
Vadorequest Mar 19, 2021
612b096
Fix roles
Vadorequest Mar 19, 2021
1872e77
Specify to which collection each GQL type belongs to
Vadorequest Mar 19, 2021
801d37e
Reformat misc
Vadorequest Mar 19, 2021
fe32d9b
Fix create shared canvas
Vadorequest Mar 19, 2021
2dc45c9
Add doc getUserByEmail no throw
Vadorequest Mar 19, 2021
0eb6cf7
Throw when obtainFaunaDBToken catch an exception
Vadorequest Mar 19, 2021
926f3d8
Split code in login to make it easier to debug
Vadorequest Mar 19, 2021
107eed4
Fix index users_by_email
Vadorequest Mar 19, 2021
65424a7
Add deploy:fake
Vadorequest Mar 19, 2021
2d71a11
Fix lastUpdatedByUserName when anonymous
Vadorequest Mar 19, 2021
30f86bd
Fix lastUpdatedByUserName when anonymous
Vadorequest Mar 19, 2021
40afb4b
Fix membership definition for roles
Vadorequest Mar 19, 2021
d917b74
Fix roles GQL (missing "Query" wrapper)
Vadorequest Mar 19, 2021
dbc9ffb
Reformat misc
Vadorequest Mar 19, 2021
63ea852
Fix create shared canvas
Vadorequest Mar 19, 2021
06b04e1
Add doc getUserByEmail no throw
Vadorequest Mar 19, 2021
4cdae66
Split code in login to make it easier to debug
Vadorequest Mar 19, 2021
05fdd20
Specify where the ref is in the result
Vadorequest Mar 19, 2021
c6ec461
Misc doc
Vadorequest Mar 19, 2021
34f6e98
Change CanvasByOwnerIndex shape (was an array of arrays, now it's sim…
Vadorequest Mar 19, 2021
ed6c252
Change CanvasByOwnerIndex shape (was an array of arrays, now it's sim…
Vadorequest Mar 19, 2021
956ba83
Add doc
Vadorequest Mar 19, 2021
16620a2
Update getting started doc
Vadorequest Mar 21, 2021
9bfbf24
Add generated graphql.ts file to project's types
Vadorequest Mar 21, 2021
cd29c46
Fix Public:create role permission
Vadorequest Mar 21, 2021
82f1f1b
Fix auto-create of shared document when it doesn't exist yet
Vadorequest Mar 21, 2021
6d0004b
Doc about manual FQL setup
Vadorequest Mar 21, 2021
8056eff
Update readme
Vadorequest Mar 21, 2021
9516099
Merge branch 'with-faunadb-auth' into with-fauna-fgu
Vadorequest Mar 21, 2021
b1d1d82
Fix typo
Vadorequest Mar 21, 2021
551479d
Merge branch 'main' into with-fauna-fgu
Vadorequest Mar 21, 2021
0bc530a
Rename indexes
Vadorequest Mar 29, 2021
48ed923
Rename more indexes
Vadorequest Mar 29, 2021
5ec82c6
Update footer description
Vadorequest Mar 29, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY=

# FaunaDB token to generate from FaunaDB, based on the "Public" role.
# Go to https://dashboard.fauna.com/ > Select DB > Shell > Run fql/setup.js (if not done already)
# You need to have created the "Public" role first (by running `yarn fauna:sync`)
# Go to https://dashboard.fauna.com/ > Select DB > Security > New Key > Role: Public | Name: PUBLIC_SHARED_FAUNABD_TOKEN
NEXT_PUBLIC_SHARED_FAUNABD_TOKEN=

Expand All @@ -24,3 +24,8 @@ CRYPTO_TOKEN_SECRET=
# Used to perform actions from the server-side (creating users, etc.)
# Go to https://dashboard.fauna.com/ > Select DB > Security > New Key > Role: Server | Name: FAUNADB_SERVER_SECRET_KEY
FAUNADB_SERVER_SECRET_KEY=

# Admin key used by Fauna GQL Upload (FGU) to upload files to FaunaDB.
# See https://github.com/Plazide/fauna-gql-upload#files-and-directories
# Go to https://dashboard.fauna.com/ > Select DB > Security > New Key > Role: Admin | Name: FGU_SECRET
FGU_SECRET=
6 changes: 6 additions & 0 deletions .fauna.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"envPath": ".env.local",
"codegen": {
"outputFile": "src/types/graphql/graphql.ts"
}
}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v14.16.0
85 changes: 82 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ It also uses the famous Next.js framework, and it's hosted on Vercel.
This RWA is meant to help beginners with any of the above-listed tools learn how to build a real app, using best-practices.
Therefore, the codebase is heavily documented, not only the README but also every file in the project.

The app allows users to create a discussion workflow in a visual way.
It displays information, questions and branching logic (if/else).
It works in real-time for better collaboration, and provides features similar as if you'd be building a Chatbot discussion.

## You want to learn?

Take a look at the **[Variants](#Variants)** below **before jumping in the source code**.
As part of my developer journey, I've reached different milestones and made different branches/PR for each of them.
If you're only interested in Reaflow, or Magic Auth, or FaunaDB Real-Time streaming, **they'll help you focus on what's of the most interest to you**.
Expand All @@ -15,7 +21,7 @@ If you're only interested in Reaflow, or Magic Auth, or FaunaDB Real-Time stream

## Online demo

[Demo](https://poc-nextjs-reaflow.vercel.app/) (automatically updated from the `master` branch).
[Demo](https://rwa-faunadb-reaflow-nextjs-magic.vercel.app/) (automatically updated from the `master` branch).

![image](https://user-images.githubusercontent.com/3807458/109431687-08bf1680-7a08-11eb-98bd-31fa91e21680.png)

Expand Down Expand Up @@ -89,6 +95,14 @@ While working on this project, I've reached several milestones with a different
Changes to the canvas are real-time and shared with everyone when not authenticated.
Changes to the canvas are real-time and shared with yourself when being authenticated. (open 2 tabs to see it in action)
Users can create an account and login using Magic Link, they'll automatically load their own document.
1. _(Current)_ [`with-fauna-fgu`](https://github.com/Vadorequest/poc-nextjs-reaflow/tree/with-faunadb-fgu)
([Demo](https://poc-nextjs-reaflow-git-with-fauna-fgu-ambroise-dhenain.vercel.app/) | [Diff](https://github.com/Vadorequest/poc-nextjs-reaflow/pull/19)):
The canvas dataset is stored in FaunaDB.
Changes to the canvas are real-time and shared with everyone when not authenticated.
Changes to the canvas are real-time and shared with yourself when being authenticated. (open 2 tabs to see it in action)
Users can create an account and login using Magic Link, they'll automatically load their own document.
Added support for quick sync of FaunaDB roles/indexes/data/functions (code as single source of truth) and GraphQL schema upload.
_This example is also available on the `main` branch._

## Roadmap

Expand All @@ -110,12 +124,15 @@ External help on those features is much welcome! Please contribute ;)

- `yarn`
- `yarn start`
- Run commands in `fql/setup.js` from the Web Shell at [https://dashboard.fauna.com/](https://dashboard.fauna.com/), this will create the FaunaDB collection, indexes, roles, etc.
- `cp .env.local.example .env.local`, and define your environment variables
- `cp .env.local.example .env.local`, and define the `FGU_SECRET` environment variable
- `yarn fauna:sync` will create all collections, indexes, roles, UDF in the Fauna database related to the `FGU_SECRET` environment variable
- Define other environment variables (`NEXT_PUBLIC_SHARED_FAUNABD_TOKEN` and `FAUNADB_SERVER_SECRET_KEY` can only be created once roles have been created during the previous step when running `yarn fauna:sync`)
- Open browser at [http://localhost:8890](http://localhost:8890)

If you deploy it to Vercel, you'll need to create Vercel environment variables for your project. (see `.env.local.example` file)

> Note: The current setup uses only one environment, the dev/staging/prod deployments all use the same database.

## Deploy your own

Deploy the example using [Vercel](https://vercel.com):
Expand All @@ -128,6 +145,68 @@ Deploy the example using [Vercel](https://vercel.com):

This section is for developers who want to understand even deeper how things work.

## Application overview

Users can be either **Guests** or **Editors**.

All requests to FaunaDB are made **from the frontend**. Even though, **they're completely secure** due to a proper combination of tokens and roles/permissions.

### Guests permissions (FaunaDB)

By default, users are guests. Guests all share the same working document and see changes made by others in real-time. They can only access (read/write) that
special shared document.

Guests use a special FaunaDB token generated from the "Public" role. They all share that same token. The token doesn't expire. Also, the token **only allows
read/write on the special shared document** (ID: "1"), see the `/fql/setup.js` file "Public" role.

Therefore, the public token, even though it's public, cannot be used to perform any other operation than read/write that single document.

### Editors permissions (FaunaDB)

Editors are authenticated users who can only access (read/write) their own documents.

A editor-related token is generated upon successful login and is used in the client to authenticate to FaunaDB. Even though the token is used by the browser,
it's still safe because the token is only readable/writeable from the server. (`httpOnly: true`)

Also, the token won't allow read/write on other documents than their owner, see the `/fql/setup.js` file "Editor" role.

### Authentication (Magic + FaunaDB + Next.js API)

Users authenticate through Magic Link (passwordless) sent to the email they used. Magic helps to simplify the authentication workflow by ensuring the users use
a valid email (they must click on a link sent to their email inbox to log in).

When the user clicks on the link in their inbox, Magic generates a `DID token`, which is then used as authentication `Bearer token` and sent to our `/api/login`
.

The `/api/login` endpoint checks the DID token and then generates a FaunaDB token (`faunaDBToken`) attached to the user. This `faunaDBToken` is then stored in
the `token` cookie (httpOnly), alongside other user-related information (UserSession object), such as their `email` and FaunaDB `ref` and `id`.

This token will then be read (`/api/user` endpoint) when the user loads the page.

_Even though there are 2 buttons (login/create account), both buttons actually do the same thing, and both can be used to sign-in and sign-up. That's because we
automatically log in new users, so whether they were an existing user or not doesn't change the authentication workflow. It made more sense (UX) to have two
different buttons, that's what people usually expect, so we made it that way._

### Workflow editor (Reaflow)

The editor provides a GUI allowing users to add "nodes" and "edges" connecting those nodes. It is meant to help them **build a workflow** using nodes such as "
Information", "Question" and "If/Else".

The workflow in itself **doesn't do anything**, it's purely visual. It typically represents a discussion a user would have with a Chatbot.

The whole app only use one page, that uses Next.js SSG mode (it's statically rendered, and the page is generated at build time, when deploying the app).

### Real-time streaming (FaunaDB)

Once the user session has been fetched (through `/api/user`), the `CanvasContainer` is rendered. One of its child component, `CanvasStream` automatically opens
a stream connection to FaunaDB on the user's document (the shared document if **Guest**, or the first document that belongs to the **Editor**).

When the stream is opened, it automatically retrieves the current state of the document and updates the local state (Recoil).

When changes are made on the document, FaunaDB send a push notification to all users subscribed to that document. This also happens when the user X updates the
document (they receives a push notification if they're the author of the changes, too). In such case, the update is being ignored for performances reasons (we
don't need to update a local state that is already up-to-date).

## Reaflow Graph (ELK)

ELKjs (and ELK) are used to draw the graph (nodes, edges).
Expand Down
4 changes: 4 additions & 0 deletions fql/examples.js → fauna/examples.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* XXX This file is simply a collection of examples and code snippets I've used.
*/

Create(
Collection('Canvas'),
{
Expand Down
31 changes: 31 additions & 0 deletions fauna/functions/current_user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FunctionResource } from 'fauna-gql-upload';
import {
CurrentIdentity,
Get,
Lambda,
Let,
Query,
Select,
Var,
} from 'faunadb';

/**
* Returns the currently authenticated user.
*
* XXX Not actually used in the app, kept as example/experimentation.
*
* @see https://github.com/Plazide/fauna-gql-upload#uploading-functions
*/
const getCurrentUserUDF: FunctionResource = {
name: 'getCurrentUser',
body: Query(
Lambda([],
Let(
{ userRef: CurrentIdentity() },
Select([], Get(Var('userRef'))),
),
),
),
};

export default getCurrentUserUDF;
26 changes: 26 additions & 0 deletions fauna/indexes/canvas_by_owner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { IndexResource } from 'fauna-gql-upload';
import { Collection } from 'faunadb';

/**
* Index to filter canvas by owner.
*
* Necessary for real-time subscription, to retrieve the canvas of the current user.
*/
const canvasByOwner: IndexResource = {
name: 'canvasByOwner',
source: Collection('Canvas'),
// Needs permission to read the Users, because "owner" is specified in the "terms" and is a Ref to the "Users" collection
permissions: {
read: Collection('Users'),
},
// Allow to filter by owner ("Users")
terms: [
{ field: ['data', 'owner'] },
],
// Index contains the Canvas ref (that's the default behavior and could be omitted)
values: [
{ field: ['ref'] },
],
};

export default canvasByOwner;
18 changes: 18 additions & 0 deletions fauna/indexes/users_by_email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { IndexResource } from 'fauna-gql-upload';
import { Collection } from 'faunadb';

/**
* Index to filter users by email
*
* Necessary for authentication, to find the user document based on their email.
*/
const usersByEmail: IndexResource = {
name: 'usersByEmail',
source: Collection('Users'),
terms: [
{ field: ['data', 'email'] },
],
unique: true,
};

export default usersByEmail;
26 changes: 20 additions & 6 deletions fql/setup.js → fauna/manual-setup.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
/**
* XXX This file is not meant to be used anymore.
* Also, the below commands won't be updated an might be outdated.
*
* It serves as a backup-up in case the Fauna GQL Upload wouldn't work anymore.
* Or, if you need to have FQL examples equivalent to what's being done by FGU.
*
* Instead of doing the below manually, you can run `yarn fauna:sync`.
*/

// ---------------------- Step 1: Create a "Users" collection ----------------------
CreateCollection({ name: 'Users' });

Expand All @@ -8,18 +18,22 @@ CreateCollection({ name: 'Canvas' });
// Index to filter users by email
// Necessary for authentication, to find the user document based on their email
CreateIndex({
name: 'users_by_email',
name: 'usersByEmail',
source: Collection('Users'),
terms: [
{ field: ['data', 'email'] },
],
// Index will return an array of Ref
values: [
{ field: ['ref'] },
],
unique: true,
});

// Index to filter canvas by owner
// Necessary for real-time subscription, to retrieve the canvas of the current user
CreateIndex({
name: 'canvas_by_owner',
name: 'canvasByOwner',
source: Collection('Canvas'),
// Needs permission to read the Users, because "owner" is specified in the "terms" and is a Ref to the "Users" collection
permissions: {
Expand All @@ -29,7 +43,7 @@ CreateIndex({
terms: [
{ field: ['data', 'owner'] },
],
// Index contains the Canvas ref (that's the default behavior and could be omitted)
// Index will return an array of Ref
values: [
{ field: ['ref'] },
],
Expand All @@ -52,8 +66,8 @@ CreateRole({
],
privileges: [
{
// Editors need read access to the canvas_by_owner index to find their own canvas
resource: Index('canvas_by_owner'),
// Editors need read access to the canvasByOwner index to find their own canvas
resource: Index('canvasByOwner'),
actions: {
read: true,
},
Expand Down Expand Up @@ -110,7 +124,7 @@ CreateRole({
// Guests can only read/write this particular document and not any other
CreateRole({
name: 'Public',
// The public role is meant to be used to generate a token which allows anyone (unauthenticated users) to update the canvas
// The public role is meant to be used to generate a key which allows anyone (unauthenticated users) to update the canvas
membership: [],
privileges: [
{
Expand Down
20 changes: 20 additions & 0 deletions fauna/predicates/onlyDeleteByOwner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { query as q } from 'faunadb';

/**
* Returns the currently authenticated user.
*
* XXX Not actually used in the app, kept as example/experimentation.
*
* @see https://github.com/Plazide/fauna-gql-upload#uploading-functions
*/
const onlyDeleteByOwner = q.Query(
q.Lambda(
'ref',
q.Equals(
q.CurrentIdentity(),
q.Select(['data', 'user'], q.Get(q.Var('ref'))),
),
),
);

export default onlyDeleteByOwner;
Loading