The RedwoodJS tutorial creates a basic blog engine. It deploys to Netlify, uses a PostgreSQL database hosted by Heroku, and introduces Authentication with Netlify Identity.
This redwoodblog
app is a modified-version of the RedwoodJS blog engine tutorial with some added tweaks:
- TailwindCSS and UI
- User Profile / Settings
- Role-based Access Control (RBAC) on Posts
- User Management via Netlify Identity API (view users)
- Contact messages get associated with user, if logged in
- Posts have an optional author and publisher set by currentUser
- Authentication using Redwood Directives
- Uses Netlify Identity Trigger Serverless function calls to assign roles when signing up
- Lists users (admin only) via Netlify Identity API
Note: This app does not store any User information in a database, but rather integrates with Netlify Identity.
You can access a demo at https://redwoodblog-with-identity.netlify.app/.
Important: This app currently uses RedwoodJS v4-canary. While we endeavor to keep this up-to-date with the latest version, there may be a short delay after a new release.
There redwoodblog defines the following roles:
- Admin
- Author
- Editor
- Publisher
Depending on the user's role(s), their access to Posts will differ.
As in the current tutorial, everyone (even those not authenticated) can view posts and submit Contact messages.
In some blogs, certain individuals might author and certain people may edit (but not author).
Publishers might be able to author, edit and delete.
Admins could do everything -- as well as access user info (but authors, editors, and publishers cannot).
Users can be assigned roles. Given the role, their access to data and perform tasks can be controlled.
It is important when applying role-based access control (RBAC) that it be applied both in the web
and api
sides.
On the web
side, you will control access on:
- Private Routes
<Private unauthenticated="home" role="admin">
<Route path="/admin/users" page={UsersPage} name="users" />
</Private>
Note: in future release, Private Routes will be able to accept a list of roles.
- Within a page, cell, or component using
hasRole()
{(hasRole('admin') || hasRole('author')) && (
<Link to={routes.newPost()} className="rw-button rw-button-green">
<div className="rw-button-icon">+</div> New Post
</Link>
)}
On the api
side:
- In a
sdl
declare your mutations with therequireAuth
directive and permitted roles
type Mutation {
createPost(input: CreatePostInput!): Post!
@requireAuth(roles: ["admin", "author", "publisher"])
updatePost(id: Int!, input: UpdatePostInput!): Post!
@requireAuth(roles: ["admin", "editor", "publisher"])
deletePost(id: Int!): Post! @requireAuth(roles: ["admin", "publisher"])
}
`
- In a
service
just mutate as normal:
export const createPost = ({ input }) => {
return db.post.create({
data: {
...input,
authorId: context.currentUser.sub,
publisherId: context.currentUser.sub,
},
})
}
-
Admins can create, update, and delete posts.
-
Authors can create and update posts.
-
Editors can update posts.
-
Publishers can create, update, and delete Posts.
Role | Create | Update | Delete | View |
---|---|---|---|---|
Admin | X | X | X | X |
Author | X | X | ||
Editor | X | X | ||
Publisher | X | X | X | X |
There is no RBAC when creating a Contact message.
However, if the user is logged in, the Contact is assigned a userId
and the email
is pulled from their currentUser
information. The Contact form does not require email
if authenticated.
- Admins can access User Management.
Role | Create | Update | Delete | View |
---|---|---|---|---|
Admin | X | |||
Author | ||||
Editor | ||||
Publisher |
The user's profile is entirely stored in the decoded access token.
By default, Netlify's JWT contains the following profile:
- sub - the User's id
- full_name - in user_metadata
- roles - in app_metadata
{
"exp": 1598058245,
"sub": "ba931ab3-8cfd-32ba-c9c2-e51df1d860d",
"email": "user@example.com",
"app_metadata": {
"provider": "email",
"roles": [
"admin",
"author",
"editor"
]
},
"user_metadata": {
"full_name": "Example User"
}
}
To get a list of users, Netlify provides a mechanism to get a short-lived token from a function's context.
One can use this token to call admi method of the GoTrue apil such as /admin/users
/ to fet aq list of all Identity users.
While there is no User
model or table, the api/src/graphql/users.sdl.js
and api/src/graphql/userMetadata.sdl.js
define the User
types.
type UserMetadata {
full_name: String!
}
type AppMetadata {
roles: [String]
}
type User {
id: String!
aud: String
role: String
email: String!
confirmed_at: DateTime
confirmation_sent_at: DateTime
recovery_sent_at: DateTime
app_metadata: AppMetadata
user_metadata: UserMetadata
created_at: DateTime!
updated_at: DateTime!
}
type Query {
userMetadata: UserMetadata!
users: [User!]!
One can they query users from a service such as users
by getting the short-lived adminToken
from the context.clientContext.identity
. With that you can call GoTrue admin methods.
import got from 'got'
import { requireAuth } from 'src/lib/auth'
export const users = async () => {
requireAuth({ roles: 'admin' })
const adminToken = context.clientContext?.identity?.token
const { body } = await got.get(
'https://<YOUR_SITE>.netlify.app/.netlify/identity/admin/users',
{
responseType: 'json',
headers: {
authorization: `Bearer ${adminToken}`,
},
}
)
return body['users']
}
You will need to Enable Identity.
To enable Identity service on your site, select the Identity tab and click Enable Identity.
This will create an Identity service instance for your site, and allow you to invite Identity users and change settings.
You can access settings for an individual Identity user by clicking their entry in the list on the site's Identity tab.
After inviting users and they confirm, you can add roles to their profile.
Roles are not Identity user editable. You can assign one or more roles of your choosing, then use them to control access to areas or functionality in your site by checking this property: "app_metadata": {"roles": ["admin"]}
.
You can trigger serverless function calls when certain Identity events happen, like when a user signs up. The following events are currently available:
identity-validate
: Triggered when an Identity user tries to sign up via Identity.identity-signup
: Triggered when an Identity user signs up via Netlify Identity. (Note: this fires for only email+password signups, not for signups via external providers e.g. Google/GitHub)identity-login
: Triggered when an Identity user logs in via Netlify Identity
To set a serverless function to trigger on one of these events, match the name of the function file to the name of the event. For example, to trigger a serverless function on identity-login events, name the function file identity-login.js
.
If you return a status other than 200 or 204 from one of these event functions, the signup or login will be blocked.
The payload in the body of an Identity event function looks like:
{
"event": "login|signup|validate",
"user": {
# an Identity user object
}
}
If your serverless function returns a 200, you can also return a JSON object with new user_metadata or app_metadata for the Identity user. For example, if you return:
{"app_metadata": {"roles": ["admin"]}}
The value of the Identity user's app metadata will be replaced with the above.
Note: To prevent external requests to event functions, Netlify generates a JSON web signature (JWS) for each event triggered by our platform, and verifies that the signature is correct before invoking an associated event function.
That means you cannot just call the function externally -- you will get a 403 Forbidden status..
On signup, we will automatically assign you roles based on your email via the "Trigger serverless functions on Identity events" feature.
If your email contains:
+author
as inexample+author-example@gmail.com
, you will be assigned theauthor
role+editor
as inexample+editor-example@gmail.com
, you will be assigned theeditor
role+publisher
as inexample+publisher-example@gmail.com
, you will be assigned thepublisher
role
See: functions/identity-signup.js
function for implemenation details.
GOTRUE_JWT_EXP - You can increase the expiration time of your JWT
SITE_NAME - Used to link to your Netlify Identity management page for your site
// redwood.toml
[web]
port = 8910
apiUrl = "/.netlify/functions"
includeEnvironmentVariables = ['SITE_NAME', 'GOTRUE_JWT_EXP']
[api]
port = 8911
includeEnvironmentVariables = ['SITE_NAME']
Note: while you may develop locally in SQLite, it's recommended in your env
to use a PostgreSQL database instead since that is what you would use in production when deployed ot Netlify.
DATABASE_URL=postgres://<username>:<password>@host/database
Prisma only supports one database provider at a time, and since we can't use SQLite in production and must switch the Postgres or MySQL, that means we need to use the same database on our local development system after making this change.
If you intend to use SQLite, be sure to change api/db/schema.prisma
to use sqlite
as the provider:
For more info on PostgreSQL setup and deployment options, please see The Database in the RedwoodJS Tutorial documentation and also Local Postgres Setup in the RedwoodJS documentation.
// api/db/schema.prisma
...
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
Note that the deployed demo app schedules a re-seed of the data daily via Repeater.dev and a Netlify Build Hook.
Any user-added posts will be removed from the demo -- but not your app.
- Redwoodjs.com: home to all things RedwoodJS.
- Tutorial: getting started and complete overview guide.
- Docs: using the Redwood Router, handling assets and files, list of command-line tools, and more.
- Redwood Community: get help, share tips and tricks, and collaborate on everything about RedwoodJS.
We use Yarn as our package manager. To get the dependencies installed, just do this in the root directory:
yarn install
yarn redwood dev
Your browser should open automatically to http://localhost:8910
to see the web app. Lambda functions run on http://localhost:8911
and are also proxied to http://localhost:8910/api/functions/*
.