Skip to content

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:

typescript
// 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 ​

typescript
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:

typescript
const canEdit = await Gate.allows("edit-post", post);

Gate.denies(ability, ...args) ​

Inverse of allows:

typescript
const cannot = await Gate.denies("edit-post", post);

Gate.authorize(ability, ...args) ​

Throws an AuthorizationError (HTTP 403) if denied - useful for concise controller code:

typescript
await Gate.authorize("edit-post", post);
// If denied, throws AuthorizationError and stops execution

Catch 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):

typescript
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():
    typescript
    export 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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
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):

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// app/Models/User.ts
export class User extends BaseModel {
    protected table = "users";

    public casts = {
        roles: "json",
        permissions: "json",
    };
}

You can now store arrays directly:

typescript
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:

  1. 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)
  2. In your User model, define getters for roles and permissions:

typescript
// 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 ​

typescript
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:

typescript
// 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:

typescript
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:

typescript
container.bind("can", (c) => c.make(CanMiddleware));

Then in routes:

typescript
Route.post("/posts/:id/publish", [PostController, "publish"])
    .middleware("can:publish-post");

RoleMiddleware - role check ​

Restrict a route to users with specific roles:

typescript
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:

typescript
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:

typescript
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:

typescript
// 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:

html
@can('publish-post')
    <button>Publish</button>
@endcan

The check passes if any of these are true:

  • user.role === 'admin' or user.role === 'superadmin'
  • user.roles includes 'admin', 'superadmin', or the ability name itself
  • user.permissions includes the ability name

@role / @endrole ​

Shows content if the user has the specified role. Checks both user.role (string) and user.roles (array):

html
@role('admin')
    <a href="/admin">Admin Panel</a>
@endrole

AuthorizationError ​

Gate.authorize() and UserGateEvaluator.authorize() throw an AuthorizationError on denial. It carries a 403 status code and can be caught like any other error:

typescript
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 ​

ToolPurpose
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
CanMiddlewareRoute middleware - ability check
RoleMiddlewareRoute middleware - role check
PermissionMiddlewareRoute middleware - direct permission check
@can / @roleTemplate directives

Released under the MIT License.