diff --git a/api/src/functions/graphql.js b/api/src/functions/graphql.js
index 2d3f47b..57ba262 100644
--- a/api/src/functions/graphql.js
+++ b/api/src/functions/graphql.js
@@ -6,9 +6,12 @@ import {
import schemas from 'src/graphql/**/*.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
+
+import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
export const handler = createGraphQLHandler({
+ getCurrentUser,
schema: makeMergedSchema({
schemas,
services: makeServices({ services }),
diff --git a/api/src/lib/auth.js b/api/src/lib/auth.js
new file mode 100644
index 0000000..402f218
--- /dev/null
+++ b/api/src/lib/auth.js
@@ -0,0 +1,144 @@
+// Define what you want `currentUser` to return throughout your app. For example,
+// to return a real user from your database, you could do something like:
+//
+// export const getCurrentUser = async ({ email }) => {
+// return await db.user.findOne({ where: { email } })
+// }
+//
+// If you want to enforce role-based access ...
+//
+// You'll need to set the currentUser's roles attributes to the
+// collection of roles as defined by your app.
+//
+// This allows requireAuth() on the api side and hasRole() in the useAuth() hook on the web side
+// to check if the user is assigned a given role or not.
+//
+// How you set the currentUser's roles depends on your auth provider and its implementation.
+//
+// For example, your decoded JWT may store `roles` in it namespaced `app_metadata`:
+//
+// {
+// 'https://example.com/app_metadata': { authorization: { roles: ['admin'] } },
+// 'https://example.com/user_metadata': {},
+// iss: 'https://app.us.auth0.com/',
+// sub: 'email|1234',
+// aud: [
+// 'https://example.com',
+// 'https://app.us.auth0.com/userinfo'
+// ],
+// iat: 1596481520,
+// exp: 1596567920,
+// azp: '1l0w6JXXXXL880T',
+// scope: 'openid profile email'
+// }
+//
+// The parseJWT utility will extract the roles from decoded token.
+//
+// The app_medata claim may or may not be namespaced based on the auth provider.
+// Note: Auth0 requires namespacing custom JWT claims
+//
+// Some providers, such as with Auth0, will set roles an authorization
+// attribute in app_metadata (namespaced or not):
+//
+// 'app_metadata': { authorization: { roles: ['publisher'] } }
+// 'https://example.com/app_metadata': { authorization: { roles: ['publisher'] } }
+//
+// Other providers may include roles simply within app_metadata:
+//
+// 'app_metadata': { roles: ['author'] }
+// 'https://example.com/app_metadata': { roles: ['author'] }
+//
+// And yet other may define roles as a custom claim at the root of the decoded token:
+//
+// roles: ['admin']
+//
+// The function `getCurrentUser` should return the user information
+// together with a collection of roles to check for role assignment:
+
+import { AuthenticationError, ForbiddenError, parseJWT } from '@redwoodjs/api'
+
+/**
+ * Use requireAuth in your services to check that a user is logged in,
+ * whether or not they are assigned a role, and optionally raise an
+ * error if they're not.
+ *
+ * @param {string=, string[]=} role - An optional role
+ *
+ * @example - No role-based access control.
+ *
+ * export const getCurrentUser = async (decoded) => {
+ * return await db.user.findOne({ where: { decoded.email } })
+ * }
+ *
+ * @example - User info is conatined in the decoded token and roles extracted
+ *
+ * export const getCurrentUser = async (decoded, { _token, _type }) => {
+ * return { ...decoded, roles: parseJWT({ decoded }).roles }
+ * }
+ *
+ * @example - User record query by email with namespaced app_metadata roles
+ *
+ * export const getCurrentUser = async (decoded) => {
+ * const currentUser = await db.user.findOne({ where: { email: decoded.email } })
+ *
+ * return {
+ * ...currentUser,
+ * roles: parseJWT({ decoded: decoded, namespace: NAMESPACE }).roles,
+ * }
+ * }
+ *
+ * @example - User record query by an identity with app_metadata roles
+ *
+ * const getCurrentUser = async (decoded) => {
+ * const currentUser = await db.user.findOne({ where: { userIdentity: decoded.sub } })
+ * return {
+ * ...currentUser,
+ * roles: parseJWT({ decoded: decoded }).roles,
+ * }
+ * }
+ */
+export const getCurrentUser = async (decoded, { _token, _type }) => {
+ return { ...decoded, roles: parseJWT({ decoded }).roles }
+}
+
+/**
+ * Use requireAuth in your services to check that a user is logged in,
+ * whether or not they are assigned a role, and optionally raise an
+ * error if they're not.
+ *
+ * @param {string=} roles - An optional role or list of roles
+ * @param {string[]=} roles - An optional list of roles
+
+ * @example
+ *
+ * // checks if currentUser is authenticated
+ * requireAuth()
+ *
+ * @example
+ *
+ * // checks if currentUser is authenticated and assigned one of the given roles
+ * requireAuth({ role: 'admin' })
+ * requireAuth({ role: ['editor', 'author'] })
+ * requireAuth({ role: ['publisher'] })
+ */
+export const requireAuth = ({ role } = {}) => {
+ if (!context.currentUser) {
+ throw new AuthenticationError("You don't have permission to do that.")
+ }
+
+ if (
+ typeof role !== 'undefined' &&
+ typeof role === 'string' &&
+ !context.currentUser.roles?.includes(role)
+ ) {
+ throw new ForbiddenError("You don't have access to do that.")
+ }
+
+ if (
+ typeof role !== 'undefined' &&
+ Array.isArray(role) &&
+ !context.currentUser.roles?.some((r) => role.includes(r))
+ ) {
+ throw new ForbiddenError("You don't have access to do that.")
+ }
+}
diff --git a/api/src/services/posts/posts.js b/api/src/services/posts/posts.js
index be53d32..8912a71 100644
--- a/api/src/services/posts/posts.js
+++ b/api/src/services/posts/posts.js
@@ -1,4 +1,5 @@
import { db } from 'src/lib/db'
+import { requireAuth } from 'src/lib/auth'
export const posts = () => {
return db.post.findMany()
@@ -11,12 +12,14 @@ export const post = ({ id }) => {
}
export const createPost = ({ input }) => {
+ requireAuth()
return db.post.create({
data: input,
})
}
export const updatePost = ({ id, input }) => {
+ requireAuth()
return db.post.update({
data: input,
where: { id },
@@ -24,6 +27,7 @@ export const updatePost = ({ id, input }) => {
}
export const deletePost = ({ id }) => {
+ requireAuth()
return db.post.delete({
where: { id },
})
diff --git a/web/package.json b/web/package.json
index b6b12fa..2a9bdad 100644
--- a/web/package.json
+++ b/web/package.json
@@ -13,6 +13,7 @@
]
},
"dependencies": {
+ "@redwoodjs/auth": "^0.19.3",
"@redwoodjs/forms": "^0.19.2",
"@redwoodjs/router": "^0.19.2",
"@redwoodjs/web": "^0.19.2",
@@ -21,6 +22,7 @@
"jquery": "^3.5.1",
"monaco-editor": "^0.20.0",
"monaco-editor-webpack-plugin": "^1.9.1",
+ "netlify-identity-widget": "^1.9.1",
"opencascade.js": "^0.1.15",
"prop-types": "^15.7.2",
"react": "^16.13.1",
diff --git a/web/src/Routes.js b/web/src/Routes.js
index 0959e4f..9fa6d36 100644
--- a/web/src/Routes.js
+++ b/web/src/Routes.js
@@ -7,7 +7,7 @@
// 'src/pages/HomePage/HomePage.js' -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
-import { Router, Route } from '@redwoodjs/router'
+import { Router, Route, Private } from '@redwoodjs/router'
const Routes = () => {
return (
@@ -18,10 +18,12 @@ const Routes = () => {
-
-
-
-
+
+
+
+
+
+
diff --git a/web/src/index.js b/web/src/index.js
index 66c7d22..6e5dc52 100644
--- a/web/src/index.js
+++ b/web/src/index.js
@@ -1,3 +1,5 @@
+import { AuthProvider } from '@redwoodjs/auth'
+import netlifyIdentity from 'netlify-identity-widget'
import ReactDOM from 'react-dom'
import { RedwoodProvider, FatalErrorBoundary } from '@redwoodjs/web'
import FatalErrorPage from 'src/pages/FatalErrorPage'
@@ -11,11 +13,15 @@ import './cascade/css/main.css'
import 'monaco-editor/min/vs/editor/editor.main.css'
import './index.css'
+netlifyIdentity.init()
+
ReactDOM.render(
-
-
-
+
+
+
+
+
,
document.getElementById('redwood-app')
)
diff --git a/yarn.lock b/yarn.lock
index c408422..ac6952d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2205,6 +2205,11 @@
lodash.omitby "^4.6.0"
merge-graphql-schemas "^1.7.6"
+"@redwoodjs/auth@^0.19.3":
+ version "0.19.3"
+ resolved "https://registry.yarnpkg.com/@redwoodjs/auth/-/auth-0.19.3.tgz#8a4f136d46e92ec58d85751c403a49647f653901"
+ integrity sha512-3yD2gyDZ9MuoAAWKI8BbvSy19LVZErystmW72jV+xk4bzwIuD6z1VtYdET5sTMK8t0RFaplHH1b1OD/He/BfuA==
+
"@redwoodjs/cli@^0.19.2":
version "0.19.2"
resolved "https://registry.yarnpkg.com/@redwoodjs/cli/-/cli-0.19.2.tgz#c9f546d76394a17b786ee5befb433d007639bb4d"
@@ -11079,6 +11084,11 @@ neo-async@^2.5.0, neo-async@^2.6.1:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+netlify-identity-widget@^1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/netlify-identity-widget/-/netlify-identity-widget-1.9.1.tgz#9e716c4b92b9f0cc041074eb86fc962f35295b46"
+ integrity sha512-9oIWjwUSdRk3SkREcZNjZaVuDDx9T/wSIXZNQsQeY4qoXic/FiXVEGgu2RU3IuA4OI3L2652xY1o+PpS03Ugaw==
+
new-github-issue-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/new-github-issue-url/-/new-github-issue-url-0.2.1.tgz#e17be1f665a92de465926603e44b9f8685630c1d"