RBAC & Roles
Grit includes a role-based access control (RBAC) system with three built-in roles. Routes can be restricted to specific roles using middleware, and the admin panel adapts its UI based on the authenticated user's role.
Overview
Every user has a role field stored as an uppercase string. Grit ships with three roles:
| Role | Value | Description |
|---|---|---|
| ADMIN | "ADMIN" | Full access. Can manage all resources, users, and system settings. |
| EDITOR | "EDITOR" | Can manage content resources. Sees the full admin panel but cannot delete users or access system pages. |
| USER | "USER" | Standard user. Sees only their profile page in the admin panel. |
Role Constants
Roles are defined as Go constants in the User model and as TypeScript types in the shared package:
Go (Backend)
const (RoleAdmin = "ADMIN"RoleEditor = "EDITOR"RoleUser = "USER")type User struct {ID uint `gorm:"primaryKey" json:"id"`FirstName string `gorm:"size:100;not null" json:"first_name"`LastName string `gorm:"size:100;not null" json:"last_name"`Email string `gorm:"uniqueIndex;size:255;not null" json:"email"`Password string `gorm:"size:255;not null" json:"-"`Role string `gorm:"size:20;default:USER" json:"role"`// ...}
TypeScript (Frontend)
export interface User {id: number;first_name: string;last_name: string;email: string;role: "ADMIN" | "EDITOR" | "USER";avatar: string;job_title: string;bio: string;active: boolean;// ...}
export const ROLES = {ADMIN: "ADMIN",EDITOR: "EDITOR",USER: "USER",} as const;
RequireRole Middleware
The RequireRole middleware checks the authenticated user's role against a list of allowed roles. It must be used after the Auth middleware.
// RequireRole returns middleware that restricts access to specific roles.func RequireRole(roles ...string) gin.HandlerFunc {return func(c *gin.Context) {userRole, exists := c.Get("user_role")if !exists {c.JSON(http.StatusForbidden, gin.H{"error": gin.H{"code": "FORBIDDEN","message": "Access denied",},})c.Abort()return}role := userRole.(string)for _, allowed := range roles {if role == allowed {c.Next()return}}c.JSON(http.StatusForbidden, gin.H{"error": gin.H{"code": "FORBIDDEN","message": "Insufficient permissions",},})c.Abort()}}
You can pass one or more roles:
// Single roleadmin.Use(middleware.RequireRole("ADMIN"))// Multiple roleseditors.Use(middleware.RequireRole("ADMIN", "EDITOR"))
Default Route Groups
Grit scaffolds three route groups in routes.go:
| Group | Marker | Access |
|---|---|---|
| Protected | // grit:routes:protected | Any authenticated user |
| Admin | // grit:routes:admin | ADMIN only |
| Custom | // grit:routes:custom | Role-restricted (via --roles flag) |
By default, grit generate resource places CRUD routes in the protected group and DELETE in the admin group. When you use the --roles flag, all routes are placed in the custom group instead.
// Protected routes (any authenticated user)protected := r.Group("/api")protected.Use(middleware.Auth(db, authService)){// Profile routes (any authenticated user)profile := protected.Group("/profile"){profile.GET("", userHandler.GetProfile)profile.PUT("", userHandler.UpdateProfile)profile.DELETE("", userHandler.DeleteProfile)}// Default resource routes go here// grit:routes:protected// Admin routes (ADMIN only)admin := protected.Group("")admin.Use(middleware.RequireRole("ADMIN")){// grit:routes:admin}// Custom role-restricted routes// grit:routes:custom}
Using the --roles Flag
When generating a resource, you can restrict all its routes to specific roles using the --roles flag:
This generates a self-contained route group with RequireRole middleware:
// Posts routes (restricted to ADMIN, EDITOR)postsGroup := protected.Group("/posts")postsGroup.Use(middleware.RequireRole("ADMIN", "EDITOR")){postsGroup.GET("", postHandler.List)postsGroup.GET("/:id", postHandler.GetByID)postsGroup.POST("", postHandler.Create)postsGroup.PUT("/:id", postHandler.Update)postsGroup.DELETE("/:id", postHandler.Delete)}
Without --roles: GET, GET/:id, POST, and PUT go in the protected group (any auth user), and DELETE goes in the admin group (ADMIN only).
With --roles: All five CRUD routes are placed in a single group restricted to the specified roles.
Profile Page
Users with the USER role are redirected from the dashboard to a dedicated profile page at /profile. The profile page includes:
- Personal Information — Edit first name, last name, and email
- Professional Information — Job title and bio
- Change Password — Update account password
- Danger Zone — Delete account with confirmation dialog
The sidebar adapts based on role:
| Role | Sidebar Shows |
|---|---|
| ADMIN / EDITOR | Dashboard, Resources, System pages, Profile |
| USER | Profile only |
Backend Profile Endpoints
Profile endpoints use the authenticated user's ID from the JWT context — no:id parameter needed:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/profile | Get current user's profile |
| PUT | /api/profile | Update profile (name, email, password, job title, bio) |
| DELETE | /api/profile | Delete account |
Adding New Roles
Use the grit add role command to add a new role across all project files in one step:
grit add role MODERATOR
This automatically updates 7 locations:
- Go model constants (models/user.go)
- Zod schema enum (schemas/user.ts)
- TypeScript union type (types/user.ts)
- ROLES constants object (constants/index.ts)
- Admin badge configuration (resources/users.ts)
- Admin table filter options
- Admin form select options
After adding a role, you can use it in route restrictions:
moderators := protected.Group("/reports")moderators.Use(middleware.RequireRole("ADMIN", "MODERATOR")){moderators.GET("", reportHandler.List)}
You may also want to update the sidebar visibility logic in components/layout/sidebar.tsx if the new role should have admin-level navigation access.