Authorization
StruxJS provides a Gate system for defining and evaluating authorization rules. It supports simple ability-based gates, policy classes, role-based access control (RBAC), and middleware for protecting routes.
The Gate Facade
Import Gate from struxjs and define your abilities in AppServiceProvider. This is the recommended place to centralize all authorization setup - gate definitions, policies, and hooks:
// app/Providers/AppServiceProvider.ts
import { Container, Gate } from "struxjs";
import { Post } from "../Models/Post.js";
export class AppServiceProvider {
public register(container: Container): void {
Gate.define("edit-post", (user, post: Post) => {
return user.id === post.user_id;
});
Gate.define("delete-post", async (user, post: Post) => {
return user.id === post.user_id || user.role === "admin";
});
}
}Checking Abilities
In controllers and services
import { Gate, Auth } from "struxjs";
export class PostController {
public async edit(request: Request, response: Response) {
const post = await Post.findOrFail(request.params.id);
// Returns true or false
if (await Gate.denies("edit-post", post)) {
return response.status(403).json({ message: "Forbidden." });
}
return response.json({ post });
}
}Gate.allows(ability, ...args)
Returns true if the currently authenticated user passes the check:
const canEdit = await Gate.allows("edit-post", post);Gate.denies(ability, ...args)
Inverse of allows:
const cannot = await Gate.denies("edit-post", post);Gate.authorize(ability, ...args)
Throws an AuthorizationError (HTTP 403) if denied - useful for concise controller code:
await Gate.authorize("edit-post", post);
// If denied, throws AuthorizationError and stops executionCatch it or let the global error handler return a 403 automatically.
Gate.forUser(user) - Checking for a specific user
By default, Gate.allows(), Gate.denies(), and Gate.authorize() automatically inspect the currently authenticated user in the active request context.
If you need to evaluate an ability for an explicit user instance (for example: in Queue Jobs, CLI commands, or when an admin checks permissions on behalf of another user), use Gate.forUser(user):
import { Gate } from "struxjs";
const user = await User.find(userId);
const evaluator = Gate.forUser(user);
// 1. Evaluate with allows() -> returns boolean
if (await evaluator.allows("edit-post", post)) {
console.log("User is permitted to edit this post.");
}
// 2. Evaluate with denies() -> returns boolean
if (await evaluator.denies("delete-post", post)) {
console.log("User cannot delete this post.");
}
// 3. Evaluate with authorize() -> throws AuthorizationError on denial
await evaluator.authorize("publish-post", post);Common Use Cases for Gate.forUser(user)
- Queue Jobs & Background Workers: Background workers run outside of an HTTP request lifecycle (no cookies or headers). Pass the job's target user to
Gate.forUser():typescriptexport class ProcessSubscriptionJob implements Job { public async handle() { const user = await User.find(this.userId); if (await Gate.forUser(user).allows("access-premium-features")) { // process premium features } } } - Console Commands & Cron Tasks: When executing CLI commands or scheduled tasks on behalf of specific user accounts.
- Administrative Impersonation & Delegation: When checking whether a team member, collaborator, or sub-account has permission to access a resource.
Policies
For models with multiple abilities, group all authorization logic into a Policy class:
// app/Policies/PostPolicy.ts
export class PostPolicy {
public viewAny(user: User): boolean {
return true; // anyone authenticated can list posts
}
public view(user: User, post: Post): boolean {
return true;
}
public create(user: User): boolean {
return user.role === "editor" || user.role === "admin";
}
public update(user: User, post: Post): boolean {
return user.id === post.user_id || user.role === "admin";
}
public delete(user: User, post: Post): boolean {
return user.id === post.user_id || user.role === "admin";
}
}Register the policy against its model. Do this in AppServiceProvider alongside your other Gate definitions:
// app/Providers/AppServiceProvider.ts
import { Container, Gate } from "struxjs";
import { Post } from "../Models/Post.js";
import { PostPolicy } from "../Policies/PostPolicy.js";
export class AppServiceProvider {
public register(container: Container): void {
// Register policies
Gate.policy(Post, PostPolicy);
// Register explicit gate abilities
Gate.define("create-post", (user) => {
return user.role === "editor" || user.role === "admin";
});
// Super-admin bypass
Gate.before((user) => {
if (user?.role === "super-admin") return true;
});
}
}When Gate.allows("update", post) is called and post is a Post instance, StruxJS automatically routes to PostPolicy.update(). Ability names are converted to camelCase: "edit-post" → editPost, "view-any" → viewAny.
Before and After Hooks
Gate.before()
Runs before all other checks. Returning a boolean short-circuits evaluation - useful for super-admin bypass:
Gate.before((user, ability) => {
if (user?.role === "super-admin") return true; // super-admin can do anything
// return undefined to continue normal evaluation
});Gate.after()
Runs after evaluation. Can override the final result:
Gate.after((user, ability, result) => {
// Log all authorization decisions
console.log(`[Auth] ${user?.email} | ${ability} | ${result ? "ALLOWED" : "DENIED"}`);
// return undefined to keep original result
});Role-Based Access Control
StruxJS does not force a rigid database schema for roles and permissions. Instead, HasRoles flexibly extracts roles and permissions from your User model regardless of how your database is designed.
Database Schema Options
You can implement RBAC using any of the following 3 common database designs:
Option 1: Single Role Column (Simplest & Most Common)
For applications where each user only possesses one primary role (e.g., admin, editor, user):
// database/migrations/xxxx_create_users_table.ts
await Schema.create("users", (table) => {
table.increments("id").primary();
table.string("name");
table.string("email").unique();
table.string("password");
table.string("role").defaultTo("user"); // e.g. "admin", "editor", "user"
table.timestamps();
});In your User model:
// app/Models/User.ts
export class User extends BaseModel {
protected table = "users";
public role!: string;
}HasRoles.hasRole(user, "admin") automatically inspects user.role === "admin".
Option 2: JSON Array Columns (Modern & Flexible)
For applications where users can hold multiple roles and custom permissions without the overhead of extra pivot tables:
// database/migrations/xxxx_create_users_table.ts
await Schema.create("users", (table) => {
table.increments("id").primary();
table.string("name");
table.string("email").unique();
table.string("password");
table.json("roles").nullable(); // e.g. ["admin", "editor"]
table.json("permissions").nullable(); // e.g. ["publish-post", "delete-post"]
table.timestamps();
});In your User model, define attribute casting:
// app/Models/User.ts
export class User extends BaseModel {
protected table = "users";
public casts = {
roles: "json",
permissions: "json",
};
}You can now store arrays directly:
await User.create({
name: "John Doe",
email: "john@example.com",
password: await Auth.hashPassword("secret"),
roles: ["admin", "editor"],
permissions: ["publish-post", "manage-users"],
});Option 3: Relational Pivot Tables (Enterprise Dynamic RBAC)
For complex enterprise applications where roles and permissions are dynamically managed via an Admin Dashboard:
Tables Structure:
users(id,name,email, ...)roles(id,name,slug)permissions(id,name,slug)role_user(user_id,role_id)permission_role(permission_id,role_id)
In your User model, define getters for
rolesandpermissions:
// app/Models/User.ts
import { BaseModel } from "struxjs";
import { Role } from "./Role.js";
export class User extends BaseModel {
protected table = "users";
public rolesRelation() {
return this.belongsToMany(Role, "role_user", "user_id", "role_id");
}
// Expose array of role slugs/names to HasRoles and Gate:
public get roles(): string[] {
const loaded = this.relations?.rolesRelation || this.relations?.roles;
return Array.isArray(loaded) ? loaded.map((r: any) => r.slug || r.name) : [];
}
}HasRoles utility
import { HasRoles } from "struxjs";
const user = await User.find(1);
// Check roles
HasRoles.hasRole(user, "admin"); // true/false
HasRoles.hasAnyRole(user, ["admin", "editor"]); // true if has at least one
HasRoles.hasAllRoles(user, ["editor", "user"]); // true if has all
// Check permissions
HasRoles.hasPermissionTo(user, "publish-post");
// Note: users with "admin" or "super-admin" role pass all permission checks
// Assign / remove roles on the model instance:
HasRoles.assignRole(user, "editor");
HasRoles.removeRole(user, "moderator");
// Grant / revoke permissions:
HasRoles.givePermissionTo(user, "publish-post", "delete-post");
HasRoles.revokePermissionTo(user, "delete-post");
// Persist updated roles/permissions to the Database:
await user.save();Gate fallback
When no explicit gate ability or policy method matches, Gate.allows() automatically falls back to HasRoles checks: it returns true if the user has the ability name as a direct permission or as a role:
// No Gate.define("admin") needed - returns true if user.roles includes "admin"
const isAdmin = await Gate.allows("admin");Route Middleware
CanMiddleware - ability check
Protect a route by requiring a Gate ability. You have 3 convenient ways to pass the gate/ability name:
1. Using instance or helper (no service provider binding required)
You can pass the ability directly to the constructor or use the fluent helper:
import { Route, CanMiddleware, can } from "struxjs";
// Option A: Pass ability to constructor
Route.post("/posts/:id/publish", [PostController, "publish"])
.middleware(new CanMiddleware("publish-post"));
// Option B: Use fluent helper function can()
Route.post("/posts/:id/publish", [PostController, "publish"])
.middleware(can("publish-post"));
// Option C: Use static factory method
Route.post("/posts/:id/publish", [PostController, "publish"])
.middleware(CanMiddleware.ability("publish-post"));2. Using string alias ("can:ability")
Register can in AppServiceProvider:
container.bind("can", (c) => c.make(CanMiddleware));Then in routes:
Route.post("/posts/:id/publish", [PostController, "publish"])
.middleware("can:publish-post");RoleMiddleware - role check
Restrict a route to users with specific roles:
import { RoleMiddleware, role } from "struxjs";
// Instance constructor
Route.get("/admin", [AdminController, "index"])
.middleware(new RoleMiddleware("admin"));
// Multiple roles using fluent helper
Route.get("/dashboard", [DashboardController, "index"])
.middleware(role("admin", "editor"));
// String alias (requires container binding)
Route.get("/dashboard", [DashboardController, "index"])
.middleware("role:admin,editor");PermissionMiddleware - direct permission check
Restrict a route to users with a specific permission:
import { PermissionMiddleware, permission } from "struxjs";
// Instance constructor
Route.post("/posts", [PostController, "store"])
.middleware(new PermissionMiddleware("publish-post"));
// Fluent helper
Route.post("/posts", [PostController, "store"])
.middleware(permission("publish-post"));
// String alias (requires container binding)
Route.post("/posts", [PostController, "store"])
.middleware("permission:publish-post");Register middlewares in AppServiceProvider if you prefer using string aliases:
import { CanMiddleware, RoleMiddleware, PermissionMiddleware } from "struxjs";
export class AppServiceProvider {
public register(container: Container): void {
container.bind("can", (c) => c.make(CanMiddleware));
container.bind("role", (c) => c.make(RoleMiddleware));
container.bind("permission", (c) => c.make(PermissionMiddleware));
}
}Combining Authentication & Authorization in Routes
You can chain authentication middleware (auth or apiauth) with authorization middleware (can, role, permission) on any route or route group:
// Web Routes (Session auth + Role check)
Route.middleware(["auth", "role:admin"]).group(() => {
Route.get("/admin", [AdminController, "index"]);
});
// API Routes (JWT Bearer Token + Role check)
Route.middleware(["apiauth", "role:admin"]).group(() => {
Route.get("/api/admin/metrics", [AdminController, "metrics"]);
});
// API Routes (JWT Bearer Token + Gate ability check)
Route.middleware(["apiauth", "can:edit-post"])
.put("/api/posts/:id", [PostController, "update"]);
// API Routes (JWT Bearer Token + Direct permission check)
Route.middleware(["apiauth", "permission:manage-users"])
.delete("/api/users/:id", [UserController, "destroy"]);DUAL-GUARD & CONTEXT SHARING (SESSION & JWT)
All authorization utilities (Gate.allows(), Gate.authorize(), CanMiddleware, RoleMiddleware, and PermissionMiddleware) automatically resolve the authenticated user from both Web Session cookies and JWT Bearer tokens (Authorization: Bearer <token>).
When apiauth verifies a JWT token, it automatically attaches the user to the request context. Downstream authorization middlewares access this user instance immediately without querying the database again.
Template Directives
In .strux view templates, use @can and @role to conditionally show UI elements. These checks are based on the user variable available in the template context.
@can / @endcan
Shows content if the user has the given permission or is an admin/superadmin. Checks both user.role (string) and user.roles (array), as well as user.permissions:
@can('publish-post')
<button>Publish</button>
@endcanThe check passes if any of these are true:
user.role === 'admin'oruser.role === 'superadmin'user.rolesincludes'admin','superadmin', or the ability name itselfuser.permissionsincludes the ability name
@role / @endrole
Shows content if the user has the specified role. Checks both user.role (string) and user.roles (array):
@role('admin')
<a href="/admin">Admin Panel</a>
@endroleAuthorizationError
Gate.authorize() and UserGateEvaluator.authorize() throw an AuthorizationError on denial. It carries a 403 status code and can be caught like any other error:
import { AuthorizationError } from "struxjs";
try {
await Gate.authorize("delete-post", post);
} catch (error) {
if (error instanceof AuthorizationError) {
return response.status(403).json({ message: error.message });
}
throw error;
}Summary
| Tool | Purpose |
|---|---|
Gate.define(ability, callback) | Define a named authorization rule |
Gate.policy(Model, PolicyClass) | Register a policy class for a model |
Gate.allows(ability, ...args) | Check - returns true/false |
Gate.denies(ability, ...args) | Inverse check |
Gate.authorize(ability, ...args) | Check and throw 403 on failure |
Gate.forUser(user) | Evaluate for a specific user |
Gate.before(callback) | Short-circuit hook (super-admin bypass) |
Gate.after(callback) | Post-evaluation hook |
HasRoles.hasRole(user, role) | Check a single role |
HasRoles.hasAnyRole(user, roles) | Check multiple roles (any) |
HasRoles.hasPermissionTo(user, perm) | Check a permission |
CanMiddleware | Route middleware - ability check |
RoleMiddleware | Route middleware - role check |
PermissionMiddleware | Route middleware - direct permission check |
@can / @role | Template directives |