-
Notifications
You must be signed in to change notification settings - Fork 396
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fe1494f
commit e979471
Showing
6 changed files
with
290 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# Path Aliases | ||
|
||
Date: 2023-08-14 | ||
|
||
Status: accepted | ||
|
||
## Context | ||
|
||
It's pretty common to configure TypeScript to have path aliases for imports. | ||
This allows you to avoid relative imports and makes it easier to move files | ||
around without having to update imports. | ||
|
||
When the Epic Stack started, we used path imports that were similar to those in | ||
the rest of the Remix ecosystem: `~/` referenced the `app/` directory. We added | ||
`tests/` to make it easier to import test utils. | ||
|
||
However, we've found that this is confusing for new developers. It's not clear | ||
what `~/` means, and seeing `import { thing } from 'tests/thing'` is confusing. | ||
I floated the idea of adding another alias for `@/` to be the app directory and | ||
or possibly just moving the `~/` to the root and having that be the only alias. | ||
But at the end of the day, we're using TypeScript which will prevent us from | ||
making mistakes and modern editors will automatically handle imports for you | ||
anyway. | ||
|
||
At first it may feel like a pain, but less tooling magic is better and editors | ||
can really help reduce the pain. Additionally, we have ESLint configured to sort | ||
imports for us so we don't have to worry about that either. Just let the editor | ||
update the imports and let ESLint sort them. | ||
|
||
## Decision | ||
|
||
Remove the path aliases from the `tsconfig`. | ||
|
||
## Consequences | ||
|
||
This requires updating all the imports that utilized the path aliases to use | ||
relative imports. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Toasts | ||
|
||
Date: 2023-08-14 | ||
|
||
Status: accepted | ||
|
||
## Context | ||
|
||
In the Epic Stack we used the Shadcn toast implementation. This worked ok, but | ||
it did require a lot of custom code for ourselves and did a poor job of managing | ||
multiple toast messages. | ||
|
||
We also had a shared `flash` session implementation for both toasts and | ||
confetti. This was overly complex. | ||
|
||
There's another library | ||
[someone told me about](https://twitter.com/ayushverma1194/status/1674848096155467788) | ||
that is a better fit. It's simpler and has an API sufficient to our use cases. | ||
|
||
It's also sufficiently customizable from a design perspective as well. And it's | ||
actively developed. | ||
|
||
## Decision | ||
|
||
Remove our own toast implementation and use the library instead. | ||
|
||
Also separate the toast and confetti session implementations. Toasts can | ||
continue to use a regular session, but confetti will be a much simpler cookie. | ||
|
||
## Consequences | ||
|
||
This will limit the level of customizability because we're now relying on a | ||
library for managing toast messages, however it also reduces the maintenance | ||
burden for users of the Epic Stack. | ||
|
||
This will also simplify the confetti implementation. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# Permissions (RBAC) | ||
|
||
Date: 2023-08-14 | ||
|
||
Status: accepted | ||
|
||
## Context | ||
|
||
Originally, the Epic Stack had a `role` and `permission` model which was quite | ||
limited in its use case. It was not very useful and not based on any real world | ||
scenario: | ||
|
||
```prisma | ||
model Role { | ||
id String @id @unique @default(cuid()) | ||
name String @unique | ||
createdAt DateTime @default(now()) | ||
updatedAt DateTime @updatedAt | ||
users User[] | ||
permissions Permission[] | ||
} | ||
model Permission { | ||
id String @id @unique @default(cuid()) | ||
name String @unique | ||
createdAt DateTime @default(now()) | ||
updatedAt DateTime @updatedAt | ||
roles Role[] | ||
} | ||
``` | ||
|
||
There are various ways to implement permissions, but a common approach is called | ||
[Role Based Access Control (RBAC)](https://auth0.com/intro-to-iam/what-is-role-based-access-control-rbac). | ||
This is a very flexible approach and can be used in many different ways. As a | ||
more established approach it's also easier to find resources to learn about and | ||
understand it. | ||
|
||
## Decision | ||
|
||
We're changing the implementation to follow a RBAC model: | ||
|
||
```prisma | ||
model Permission { | ||
id String @id @default(cuid()) | ||
action String // e.g. create, read, update, delete | ||
entity String // e.g. note, user, etc. | ||
access String // e.g. own or any | ||
description String @default("") | ||
createdAt DateTime @default(now()) | ||
updatedAt DateTime @updatedAt | ||
roles Role[] | ||
@@unique([action, entity, access]) | ||
} | ||
model Role { | ||
id String @id @default(cuid()) | ||
name String @unique | ||
description String @default("") | ||
createdAt DateTime @default(now()) | ||
updatedAt DateTime @updatedAt | ||
users User[] | ||
permissions Permission[] | ||
} | ||
``` | ||
|
||
This allows us to have much more fine grained control over our permissions. | ||
Additionally, we can create utilities for determining whether a user has | ||
permission to perform an action and disallow them from doing so if they do not. | ||
|
||
## Consequences | ||
|
||
This is a breaking change for the Epic Stack. Anyone wanting to adopt this | ||
permissions model will need to perform a database migration. However, it's | ||
important that we make this change now because the previous model was not great. | ||
This one is. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Remix Auth | ||
|
||
Date: 2023-08-14 | ||
|
||
Status: accepted | ||
|
||
## Context | ||
|
||
At the start of Epic Stack, we were using | ||
[remix-auth-form](https://github.com/sergiodxa/remix-auth-form) for our | ||
username/password auth solution. This worked fine, but it really didn't give us | ||
any value over handling the auth song-and-dance ourselves. | ||
|
||
## Decision | ||
|
||
Instead of relying on remix-auth for handling authenticating the user's login | ||
form submission, we'll manage it ourselves. | ||
|
||
## Consequences | ||
|
||
This mostly allows us to remove some code. However, we're going to be keeping | ||
remix auth around for GitHub Auth |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# GitHub Auth | ||
|
||
Date: 2023-08-14 | ||
|
||
Status: accepted | ||
|
||
## Context | ||
|
||
Many applications require integrating with third party authentication providers. | ||
For this reason, we want to support the idea of "connections" as a built-in part | ||
of the Epic Stack. | ||
|
||
There are many different providers we could support, but many people need to | ||
support more than just one. By building things in a way that allows us to | ||
support more than just a single auth provider, it allows us to also make it easy | ||
to swap to a different provider as needed. | ||
|
||
Many auth providers support OAuth2, but increasingly, many are also supporting | ||
OpenID Connect. OpenID Connect is a layer on top of OAuth2 that provides a | ||
standardized way to get user information from the auth provider. | ||
|
||
Sadly, GitHub (a common popular auth provider for many developer-focused apps) | ||
does not support OpenID, however, by using | ||
[`remix-auth`](https://github.com/sergiodxa/remix-auth), we can easily support | ||
GitHub as a built-in implementation and allow people to swap it out for whatever | ||
OAuth2 or OIDC auth provider they have (if OIDC, they can use | ||
[web-oidc](https://github.com/sergiodxa/web-oidc)). | ||
|
||
## Decision | ||
|
||
We will update the database schema to support multiple auth providers with a | ||
model called `Connection`: | ||
|
||
```prisma | ||
model Connection { | ||
id String @id @default(cuid()) | ||
providerName String | ||
providerId String @unique | ||
createdAt DateTime @default(now()) | ||
updatedAt DateTime @updatedAt | ||
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) | ||
userId String | ||
@@unique([providerName, providerId]) | ||
@@unique([providerId, userId]) | ||
} | ||
``` | ||
|
||
We'll also build the appropriate callback URL handlers and UI to allow people to | ||
manage their connections. | ||
|
||
## Consequences | ||
|
||
With third party auth, this means that users may not have passwords. So we'll | ||
need to handle that situation and allow users to onboard without the use of | ||
passwords. We'll also need to prevent them from deleting all their connections | ||
until they've created a password. | ||
|
||
There are a number of states for the user to be in within the callback as well | ||
which all will need to be considered. All of these states will be tested to | ||
ensure they continue to function properly as people tune things for their needs. | ||
|
||
Additionally, we'll need to account for the fact that some folks don't want to | ||
set up the GitHub login flow from the start (to keep in line with our | ||
[Minimize Setup Friction guiding principle](../guiding-principles.md)), so we'll | ||
have to make sure that the app still runs properly without GitHub auth | ||
configured. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# Permissions | ||
|
||
The Epic Stack's Permissions model takes after | ||
[Role-Based Access Control (RBAC)](https://auth0.com/intro-to-iam/what-is-role-based-access-control-rbac). | ||
Each user has a set of roles, and each role has a set of permissions. A user's | ||
permissions are the union of the permissions of all their roles (with the more | ||
permissive permission taking precedence). | ||
|
||
The default development seed creates fine-grained permissions that include | ||
`create`, `read`, `update`, and `delete` permissions for `user` and `note` with | ||
the access of `own` and `any`. The default seed also creates `user` and `admin` | ||
roles with the sensible permissions for those roles. | ||
|
||
You can combine these permissions in different ways to support different roles | ||
for different personas of users of your application. | ||
|
||
The Epic Stack comes with built-in utilities for working with these permissions. | ||
Here are some examples to give you an idea: | ||
|
||
```ts | ||
// server-side only utilities | ||
const userCanDeleteAnyUser = await requireUserWithPermission( | ||
request, | ||
'delete:user:any', | ||
) | ||
const userIsAdmin = await requireUserWithRole(request, 'admin') | ||
``` | ||
|
||
```ts | ||
// UI utilities | ||
const user = useUser() | ||
const userCanCreateTheirOwnNotes = userHasPermission(user, 'create:note:own') | ||
const userIsUser = userHasRole(user, 'user') | ||
``` | ||
|
||
There is currently no UI for managing permissions, but you can use prisma studio | ||
for establishing these. | ||
|
||
## Seeding the production database | ||
|
||
Check [the deployment docs](./deployment.md) for instructions on how to seed the | ||
production database with the roles you want. |