add authentication
This commit is contained in:
@@ -6,9 +6,12 @@ import {
|
|||||||
|
|
||||||
import schemas from 'src/graphql/**/*.{js,ts}'
|
import schemas from 'src/graphql/**/*.{js,ts}'
|
||||||
import services from 'src/services/**/*.{js,ts}'
|
import services from 'src/services/**/*.{js,ts}'
|
||||||
|
|
||||||
|
import { getCurrentUser } from 'src/lib/auth'
|
||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
|
|
||||||
export const handler = createGraphQLHandler({
|
export const handler = createGraphQLHandler({
|
||||||
|
getCurrentUser,
|
||||||
schema: makeMergedSchema({
|
schema: makeMergedSchema({
|
||||||
schemas,
|
schemas,
|
||||||
services: makeServices({ services }),
|
services: makeServices({ services }),
|
||||||
|
|||||||
144
api/src/lib/auth.js
Normal file
144
api/src/lib/auth.js
Normal file
@@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
|
import { requireAuth } from 'src/lib/auth'
|
||||||
|
|
||||||
export const posts = () => {
|
export const posts = () => {
|
||||||
return db.post.findMany()
|
return db.post.findMany()
|
||||||
@@ -11,12 +12,14 @@ export const post = ({ id }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const createPost = ({ input }) => {
|
export const createPost = ({ input }) => {
|
||||||
|
requireAuth()
|
||||||
return db.post.create({
|
return db.post.create({
|
||||||
data: input,
|
data: input,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updatePost = ({ id, input }) => {
|
export const updatePost = ({ id, input }) => {
|
||||||
|
requireAuth()
|
||||||
return db.post.update({
|
return db.post.update({
|
||||||
data: input,
|
data: input,
|
||||||
where: { id },
|
where: { id },
|
||||||
@@ -24,6 +27,7 @@ export const updatePost = ({ id, input }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const deletePost = ({ id }) => {
|
export const deletePost = ({ id }) => {
|
||||||
|
requireAuth()
|
||||||
return db.post.delete({
|
return db.post.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@redwoodjs/auth": "^0.19.3",
|
||||||
"@redwoodjs/forms": "^0.19.2",
|
"@redwoodjs/forms": "^0.19.2",
|
||||||
"@redwoodjs/router": "^0.19.2",
|
"@redwoodjs/router": "^0.19.2",
|
||||||
"@redwoodjs/web": "^0.19.2",
|
"@redwoodjs/web": "^0.19.2",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"jquery": "^3.5.1",
|
"jquery": "^3.5.1",
|
||||||
"monaco-editor": "^0.20.0",
|
"monaco-editor": "^0.20.0",
|
||||||
"monaco-editor-webpack-plugin": "^1.9.1",
|
"monaco-editor-webpack-plugin": "^1.9.1",
|
||||||
|
"netlify-identity-widget": "^1.9.1",
|
||||||
"opencascade.js": "^0.1.15",
|
"opencascade.js": "^0.1.15",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// 'src/pages/HomePage/HomePage.js' -> HomePage
|
// 'src/pages/HomePage/HomePage.js' -> HomePage
|
||||||
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
|
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
|
||||||
|
|
||||||
import { Router, Route } from '@redwoodjs/router'
|
import { Router, Route, Private } from '@redwoodjs/router'
|
||||||
|
|
||||||
const Routes = () => {
|
const Routes = () => {
|
||||||
return (
|
return (
|
||||||
@@ -18,10 +18,12 @@ const Routes = () => {
|
|||||||
<Route path="/parts/{id:Int}" page={PartPage} name="part" />
|
<Route path="/parts/{id:Int}" page={PartPage} name="part" />
|
||||||
<Route path="/parts" page={PartsPage} name="parts" />
|
<Route path="/parts" page={PartsPage} name="parts" />
|
||||||
<Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />
|
<Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />
|
||||||
|
<Private unauthenticated="home">
|
||||||
<Route path="/admin/posts/new" page={NewPostPage} name="newPost" />
|
<Route path="/admin/posts/new" page={NewPostPage} name="newPost" />
|
||||||
<Route path="/admin/posts/{id:Int}/edit" page={EditPostPage} name="editPost" />
|
<Route path="/admin/posts/{id:Int}/edit" page={EditPostPage} name="editPost" />
|
||||||
<Route path="/admin/posts/{id:Int}" page={PostPage} name="post" />
|
<Route path="/admin/posts/{id:Int}" page={PostPage} name="post" />
|
||||||
<Route path="/admin/posts" page={PostsPage} name="posts" />
|
<Route path="/admin/posts" page={PostsPage} name="posts" />
|
||||||
|
</Private>
|
||||||
<Route path="/about" page={AboutPage} name="about" />
|
<Route path="/about" page={AboutPage} name="about" />
|
||||||
<Route path="/" page={PartsPage} name="home" />
|
<Route path="/" page={PartsPage} name="home" />
|
||||||
<Route notfound page={NotFoundPage} />
|
<Route notfound page={NotFoundPage} />
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { AuthProvider } from '@redwoodjs/auth'
|
||||||
|
import netlifyIdentity from 'netlify-identity-widget'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import { RedwoodProvider, FatalErrorBoundary } from '@redwoodjs/web'
|
import { RedwoodProvider, FatalErrorBoundary } from '@redwoodjs/web'
|
||||||
import FatalErrorPage from 'src/pages/FatalErrorPage'
|
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 'monaco-editor/min/vs/editor/editor.main.css'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
|
netlifyIdentity.init()
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<FatalErrorBoundary page={FatalErrorPage}>
|
<FatalErrorBoundary page={FatalErrorPage}>
|
||||||
|
<AuthProvider client={netlifyIdentity} type="netlify">
|
||||||
<RedwoodProvider>
|
<RedwoodProvider>
|
||||||
<Routes />
|
<Routes />
|
||||||
</RedwoodProvider>
|
</RedwoodProvider>
|
||||||
|
</AuthProvider>
|
||||||
</FatalErrorBoundary>,
|
</FatalErrorBoundary>,
|
||||||
document.getElementById('redwood-app')
|
document.getElementById('redwood-app')
|
||||||
)
|
)
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -2205,6 +2205,11 @@
|
|||||||
lodash.omitby "^4.6.0"
|
lodash.omitby "^4.6.0"
|
||||||
merge-graphql-schemas "^1.7.6"
|
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":
|
"@redwoodjs/cli@^0.19.2":
|
||||||
version "0.19.2"
|
version "0.19.2"
|
||||||
resolved "https://registry.yarnpkg.com/@redwoodjs/cli/-/cli-0.19.2.tgz#c9f546d76394a17b786ee5befb433d007639bb4d"
|
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"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
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:
|
new-github-issue-url@^0.2.1:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/new-github-issue-url/-/new-github-issue-url-0.2.1.tgz#e17be1f665a92de465926603e44b9f8685630c1d"
|
||||||
|
|||||||
Reference in New Issue
Block a user