Skip to content

Commit

Permalink
With FaunaDB Authentication (#18)
Browse files Browse the repository at this point in the history
Co-authored-by: Dhenain Ambroise <ambroise.dhenain@gmail.com>
Co-authored-by: Dan Hen <37185115+Dan-Hen@users.noreply.github.com>
  • Loading branch information
Vadorequest and Dan-Hen committed Mar 18, 2021
1 parent e8a2dc0 commit 7625102
Show file tree
Hide file tree
Showing 94 changed files with 4,701 additions and 548 deletions.
26 changes: 26 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# XXX Duplicate this file as ".env.local" and fill-in below environment variables.
# This file is used for local development only (localhost).
# You'll need to add those environment variables as "Vercel environment variables" manually, too.

# Magic Link provides a "publishable key" which is used on the browser (and thus, public).
# Go to https://dashboard.magic.link/ > API Keys > Test "Publishable key"
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)
# Go to https://dashboard.fauna.com/ > Select DB > Security > New Key > Role: Public | Name: PUBLIC_SHARED_FAUNABD_TOKEN
NEXT_PUBLIC_SHARED_FAUNABD_TOKEN=

# Magic Link provides a "secret key" which must only be used from the server.
# Go to https://dashboard.magic.link/ > API Keys > Test "Secret key"
MAGIC_SECRET_KEY=

# Used by @hapi/iron, must be a string of 32 characters min. Can be any value.
# Changing this secret will invalidate all existing user sessions. (they'll have to log in again)
# You can generate a string using https://passwordsgenerator.net/ (recommended 32 chars, no special chars)
CRYPTO_TOKEN_SECRET=

# Server secret key for FaunaDB.
# 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=
208 changes: 179 additions & 29 deletions README.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions fql/examples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Create(
Collection('Canvas'),
{
data: {
owner: Ref(Collection("Users"), "292674252603130373"),
nodes: [],
edges: [],
}
},
)

Lambda("ref", Equals(
CurrentIdentity(),
Select(["data", "owner"], Get(Var("ref")))
))
148 changes: 148 additions & 0 deletions fql/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// ---------------------- Step 1: Create a "Users" collection ----------------------
CreateCollection({ name: 'Users' });

// ---------------------- Step 2: Create "Canvas" collection ----------------------
CreateCollection({ name: 'Canvas' });

// ---------------------- Step 3: Create indexes ----------------------
// Index to filter users by email
// Necessary for authentication, to find the user document based on their email
CreateIndex({
name: 'users_by_email',
source: Collection('Users'),
terms: [
{ field: ['data', 'email'] },
],
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',
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'] },
],
});

// ---------------------- Step 4: Create roles ----------------------

// The "Editor" role is assigned to all authenticated users
// It is automatically assigned when a user is authenticated, because it defines "membership" to the Users collection
// It is secure because the token is generated upon login on the server-side and stored in a "httpOnly" cookie that can only be read/written on the server-side
// The token is specific to the user and is used on the frontend
// The token only allows the user to read/write documents that belongs to him
CreateRole({
name: 'Editor',
// All users should be editors (will apply to authenticated users only).
membership: {
resource: Collection('Users'),
},
privileges: [
{
// Editors need read access to the canvas_by_owner index to find their own canvas
resource: Index('canvas_by_owner'),
actions: {
read: true,
},
},
{
resource: Collection('Canvas'),
actions: {
// Editors should be able to read (+ history) of Canvas documents that belongs to them.
read: Query(
Lambda('ref', Equals(
CurrentIdentity(),
Select(['data', 'owner'], Get(Var('ref'))),
)),
),
history_read: Query(
Lambda('ref', Equals(
CurrentIdentity(),
Select(['data', 'owner'], Get(Var('ref'))),
)),
),
// Editors should be able to edit only Canvas documents that belongs to them
write: Lambda(
["oldData", "newData", "ref"],
And(
// The owner in the current data (before writing them) must be the current user
Equals(
CurrentIdentity(),
Select(["data", "owner"], Var("oldData"))
),
// The owner must not change
Equals(
Select(["data", "owner"], Var("oldData")),
Select(["data", "owner"], Var("newData"))
)
)
),
// Editors should be able to create only Canvas documents that belongs to them
create: Lambda("values", Equals(
CurrentIdentity(),
Select(["data", "owner"], Var("values")))
),
},
},
],
});

// The "Public" role is assigned to anyone who isn't authenticated
// It doesn't use "membership" (unlike "Editor" role) but a token created manually that doesn't expire
// It is secure because the token only grant access to the special document of id "1", which is shared amongst all guests
// 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
membership: {},
privileges: [
{
resource: Collection('Canvas'),
actions: {
// Guests should only be allowed to read the Canvas of id "1"
read: Query(
Lambda('ref',
Equals(
'1',
Select(['id'], Var('ref'),
),
),
),
),
// Guests should only be allowed to update the Canvas of id "1"
write: Lambda(
['oldData', 'newData', 'ref'],
Equals(
'1',
Select(['id'], Var('ref')),
),
),
// Guests should only be allowed to create the Canvas of id "1"
create: Lambda('values',
Equals(
'1',
Select(['ref', 'id'], Var('values')),
),
),
// Creating a record with a custom ID requires history_write privilege
// See https://fauna-community.slack.com/archives/CAKNYCHCM/p1615413941454700
history_write: Lambda(
['ref', 'ts', 'action', 'data'],
Equals(
'1',
Select(['id'], Var('ref')),
),
),
},
},
],
});
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,37 @@
"@fortawesome/free-brands-svg-icons": "5.15.2",
"@fortawesome/free-solid-svg-icons": "5.15.2",
"@fortawesome/react-fontawesome": "0.1.14",
"@hapi/iron": "6.0.0",
"@magic-sdk/admin": "1.3.0",
"@types/lodash.clonedeep": "4.5.6",
"@types/lodash.isempty": "4.4.6",
"@types/lodash.merge": "4.6.6",
"@types/lodash.now": "4.0.6",
"@types/lodash.size": "4.2.6",
"@unly/utils": "1.0.3",
"@welldone-software/why-did-you-render": "6.0.5",
"animate.css": "4.1.1",
"classnames": "2.2.6",
"cookie": "0.4.1",
"deep-object-diff": "1.1.0",
"faunadb": "4.1.1",
"faunadb-fql-lib": "0.13.0",
"framer-motion": "3.2.1",
"lodash.capitalize": "4.2.1",
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"lodash.filter": "4.6.0",
"lodash.includes": "4.3.0",
"lodash.isempty": "4.4.0",
"lodash.isequal": "4.5.0",
"lodash.isobjectlike": "4.0.0",
"lodash.merge": "4.6.2",
"lodash.now": "4.0.2",
"lodash.remove": "4.7.0",
"lodash.size": "4.2.0",
"lodash.some": "4.6.0",
"lodash.sortby": "4.7.0",
"magic-sdk": "4.1.1",
"next": "10.0.6",
"rdk": "5.0.6",
"react": "17.0.1",
Expand All @@ -51,16 +60,20 @@
"recoil-devtools-dock": "^0.1.6",
"recoil-devtools-log-monitor": "^0.2.7",
"recoil-devtools-logger": "^0.1.5",
"swr": "0.4.2",
"use-debounce": "6.0.0",
"uuid": "8.3.2"
},
"devDependencies": {
"@emotion/babel-plugin": "11.1.2",
"@types/classnames": "2.2.11",
"@types/cookie": "0.4.0",
"@types/lodash.capitalize": "4.2.6",
"@types/lodash.debounce": "4.0.6",
"@types/lodash.filter": "4.6.6",
"@types/lodash.includes": "4.3.6",
"@types/lodash.isequal": "4.5.5",
"@types/lodash.isobjectlike": "4.0.6",
"@types/lodash.remove": "4.7.6",
"@types/lodash.some": "4.6.6",
"@types/lodash.sortby": "4.7.6",
Expand Down
59 changes: 59 additions & 0 deletions src/components/Animated3Dots.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';

export type Props = {
/**
* Color of the dots.
*
* @default white
*/
fill?: string;
};

/**
* An animated composant featuring 3 animated dots "...".
*
* Each dot is animated separately, in alternation.
* Requires animate.css library.
*
* @see https://animate.style
*/
const Animated3Dots = (props: Props): JSX.Element => {
return (
<svg
id="AnimatedBubble_svg__Calque_1"
x={0}
y={0}
viewBox="0 0 19 5"
width="20px"
style={{
overflow: 'visible',
paddingTop: 5,
marginLeft: 5,
}}
xmlSpace="preserve"
fill={'white'}
{...props}
>
<circle
className="animate__animated animate__fadeIn animate__infinite delay-200ms"
cx={2.783}
cy={2.796}
r={2.153}
/>
<circle
className="animate__animated animate__fadeIn animate__infinite delay-400ms"
cx={9.576}
cy={2.796}
r={2.153}
/>
<circle
className="animate__animated animate__fadeIn animate__infinite delay-600ms"
cx={16.369}
cy={2.796}
r={2.153}
/>
</svg>
);
};

export default Animated3Dots;
Loading

1 comment on commit 7625102

@vercel
Copy link

@vercel vercel bot commented on 7625102 Mar 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.