Hi Tom, I'm currently building a task management application, and I find it pretty similiar to your project in termes of "architecture". I have a workspace model that could be assimilated to your organization model. So when it comes to setting the active workspace feature I did the same thing as you did, same for ACL, abilities etc. So basically if the authenticated user is not a member of a workspace, it doesn't appear in his list of workspaces and then cannot set it as active.
But I'm facing a problem when I tried to get access to a workspace page, that the authenticated user is not a member of by putting the workspace id in the URL. Since my user has another active workspace in which his role is ADMIN, then, the user can access the workspace even if he's not a member of it.
I was asking myself if you managed it, if I missed something, or if it's "normal" and I need to modify how I check the user's role and stop using the active workspace ?
Sorry, I think this is not very clear, pretty hard to explain it since I'm a bit lost in all of those concepts 😅
Ready to get started?
Join Adocasts Plus for $8/mo, or sign into an existing Adocasts Plus account, to get access to all of our lessons.
Applying Our Server-Side Authorization Checks
We'll use our access controls to add authorization checks to our controllers where needed. This will help ensure members can't update, delete, or invite users.
Join the Discussion 12 comments
-
1
-
Responding to emilien
Hi emilien! I think I'm following correctly, but let me know if this sounds off base.
So, we manage this with our organizations by querying the active org through the authenticated user, which ensures the user is a member of the org. If the user sets an id of an org that they aren't a member of, our query won't find a match and will instead overwrite the user's active org to the first org found that they're a member of.
For our application, the active organization is determined via a cookie. It sounds like you might have this set up as a route parameter.
If that's the case, ensure you attempt to query the workspace through the authenticated user. If a workspace is not found, you'll want to either:
Throw an unauthorized exception to prevent the user from working with the workspace. You can then offer some button to take them back to safety.
Or, find a valid workspace for the user and redirect them to it. You'll also want to notify them that they did not have access to the previously requested workspace.
The first option there probably being the best as it will ensure the user knows what happened. You don't want to continue onward as the URL contains an invalid id for the user, which could accidentally be used to load something or on subsequent requests sent from whatever page is rendered as a result.
Hopefully that's at least a little on point and helps a little. If not, please feel free to share your query/flow, or a representation of it, and I can try and see if anything stands out.
1-
Responding to tomgobich
Hi Tom, thanks for your reply. After reading my first comment, I think I was not clear at all 😅
I also use a cookie for the active workspace.I'll try to explain my problem a little bit better :
user A is member and admin of workspace A → id = 1
user B is member and admin of workspace B → id = 2
Everything is fine for the moment, according to my ACL if the user A goes to '/workspaces/1' he can see the page. Same for user B if '/workspaces/2'. This URL will trigger the
show()
method in WorkspacesController. You won't see the use of the ACL in this method in the repo cause I did not commited, but I used it like you do in your tutorial.My problem is that if for some reasons, the user B goes to '/workspaces/1', he can still view the workspace, however he's not a member of it. I reproduced the same logic as you did to get the active workspace
I probably have missed something or there is a point that I do not understand. Maybe you already pointed it out in your answer, and if that's the case, I'm sorry. But I'm a bit confused, it's the first time that I have to work with permissions.
1-
Responding to emilien
Alrighty, I think I see what's going on! Where your project differs is that your
/workspaces/:id
route merely renders a page. In our project, and even in yourmain
branch, this instead sets the active org/workspace in our PlotMyCourse & on your main branch, then redirects back to the dashboard.Where I think the confusion comes in is the
/:id
in your/workspaces/:id
route is merely symbolic since you're actually loading the workspace via the user's cookie. So, the displayed workspace is whatever is stored in the cookie and not what is provided via the route parameter id. Things do seem to still be working, the issue is that the id in the url can differ from what is actually displayed. Which, can lead to issues if you were to ever actually use the url's id for something.So, to prevent that possibility, I would either
Stick to the cookie and get rid of the
/:id
in your/workspaces/:id
route to instead have something like:/workspace
or/workspaces/show
Or, drop the cookie and instead use the id route parameter.
To add, there isn't an issue with using
/workspaces/:id/active
to set the active workspace. The issue is that the new/workspaces/:id
route that shows the workspace has two spots it could pull the id from; the id route parameter in the url and the cookie, and those can differ from one another.Here's a video walkthrough of the behavior I was seeing on your
feat/workspace-roles
branch. This shows how user-a could request workspace b via/workspaces/2
but actually still saw workspace a; meaning the id in the url did not match the workspace used.Here's another walkthrough of the behavior I saw on your
main
branch. In this branch everything works fine.Hope this helps!!
1-
Responding to tomgobich
Of course this helps, could have spent a lot of time on this. That is so obvious with your explanation… Thanks a lot. Oh I feel so dumb, I created a route with a parameter that I don't use…
I think I also need to take time to document the flow of setting an active workspace etc and understand it much better. Completely forgot that the middleware kind of "block" access to workspaces that the user is not a member of.
Thanks again Tom ! Loving your tutorials and your fast answers to comments ! 😊
1-
Responding to emilien
Awesome, glad to hear that helped clear things up! Ah, please don't feel dumb about that, it is just a small oversight. We've all done a lot worse!! 😊
That sounds like a great plan! Having a document/diagram of how things work would definitely help. That's something I'd like to make a more concerted effort to do in-lesson.
Anytime emilien!! Always happy to help where I can! 😊
1-
Responding to tomgobich
I've tried to make a flowchart of the middleware and how it uses both, GetActive and SetActive actions. It is related to my use case (workspace and not organization) though, but I think the logic stays the same.
I don't know if you'll find it useful, but just in case, here it is. Let me know if there are any errors or inconsistencies. It was really interesting to do and taught me a lot, maybe I could help with this kind of thing if needed, don't hesitate, it would be a pleasure.1-
Responding to emilien
Oh nice! Yeah, this looks spot on, nicely done emilien! Thank you, I really appreciate that offer! I'm happy to hear that going through this was fruitful!! 😃
1
-
-
-
-
-
Hi Tom, after creating our exceptions like ForbiddenException, I guess that the best way to use them in handler.ts with
error instance of ForbiddenException
but what about a custom specific exception like a DB Conflict without vine for example. It feels weird to add specific errors there with a flashErrors but wrap the action in a try catch feels messy… Also I am probably missing a clean way to handle the exception properly when I don't need to redirect the user but add an error to display.
Just an example to illustrate:// action_example.ts throw new ConflictException('You or one of your associate already have this item registered.') // handler.ts async handle(error: unknown, ctx: HttpContext) { if (error instanceof ConflictException) { ctx.session.flashErrors({ [error.code!]: error.message }) // to access it using errors.E_CONFLICT return ctx.response.redirect().back() } return super.handle(error, ctx) } // conflict_exception.ts import { Exception } from '@adonisjs/core/exceptions' export default class ConflictException extends Exception { static status = 409 static code = 'E_CONFLICT' }
Copied!1-
Responding to memsbdm
Hi memsbdm!
Unless you're looking to handle these exceptions a specific way, the built in status page functionality of AdonisJS should suffice for these! These status pages work the same way in Inertia as they do in standard AdonisJS.
Any status codes within the range specified via the
statusPages
object, which is located within yourapp/exceptions/handle.ts
file, will render out the designated page for that status code to alert your user of the exception.import app from '@adonisjs/core/services/app' import { HttpContext, ExceptionHandler } from '@adonisjs/core/http' import type { StatusPageRange, StatusPageRenderer } from '@adonisjs/core/types/http' export default class HttpExceptionHandler extends ExceptionHandler { // ... /** * Status pages are used to display a custom HTML pages for certain error * codes. You might want to enable them in production only, but feel * free to enable them in development as well. */ protected renderStatusPages = app.inProduction /** * Status pages is a collection of error code range and a callback * to return the HTML contents to send as a response. */ protected statusPages: Record<StatusPageRange, StatusPageRenderer> = { '404': (error, { inertia }) => inertia.render('errors/not_found', { error }), '500..599': (error, { inertia }) => inertia.render('errors/server_error', { error }), } // ... }
Copied!If, as shown in your example, you're looking to redirect the user back to the previous page when the exception is raised and alert them via a toast (or something similar) instead. You could create an exception with a handle method doing exactly that, then extend that exception for your
ConflictException
and any others you've created.I believe that'd look something like the below!
// app/exceptions/exception_redirect_handler export default class ExceptionRedirectHandler extends Exception { async handle(error: unknown, ctx: HttpContext) { ctx.session.flashErrors({ [error.code!]: error.message }) // to access it using errors.E_CONFLICT return ctx.response.redirect().back() } }
Copied!export default class ConflictException extends ExceptionRedirectHandler { static status = 409 static code = 'E_CONFLICT' }
Copied!- app
- exceptions
- conflect_exception.ts
1-
Responding to tomgobich
-
Responding to memsbdm
-
-