Skip to content

Roles & Permissions

Moon ERP uses spatie/laravel-permission for role-based access control. Roles are assigned to users and determine what actions they can perform across the system.

Purpose

  • Assign roles to users to control access to modules and features
  • Define granular permissions for fine-grained authorization
  • Protect API endpoints by checking user roles and permissions
  • Support common organizational roles out of the box
  • Create custom roles with tailored permission sets

API Endpoints

MethodEndpointDescription
GET/api/core/rolesList all roles
POST/api/core/rolesCreate a custom role
GET/api/core/roles/{id}Get a single role
PUT/api/core/roles/{id}Update a role
DELETE/api/core/roles/{id}Delete a custom role
GET/api/core/permissionsList all permissions grouped by module

Built-in Roles

The system ships with the following built-in (protected) roles that cannot be deleted:

RoleDescription
ownerCompany owner with full access
adminFull access to all modules and features
managerManagerial access across modules
accountantAccess to accounting, journal entries, reports
cashierPOS and cash register access
employeeBasic access, assigned by default on registration

List Roles

Retrieve all roles with their permissions, user count, and protection status.

bash
curl -X GET https://moon-erp.test/api/core/roles \
  -H "Accept: application/json" \
  -H "Authorization: Bearer {token}"
dart
final response = await http.get(
  Uri.parse('https://moon-erp.test/api/core/roles'),
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer $token',
  },
);

Response:

json
{
  "data": [
    {
      "id": 1,
      "name": "owner",
      "guard_name": "web",
      "is_protected": true,
      "permissions": ["core.company.view", "core.users.view"],
      "users_count": 1,
      "created_at": "2026-02-20T00:00:00.000000Z"
    },
    {
      "id": 7,
      "name": "supervisor",
      "guard_name": "web",
      "is_protected": false,
      "permissions": ["core.dashboard.view"],
      "users_count": 3,
      "created_at": "2026-02-21T00:00:00.000000Z"
    }
  ]
}
FieldTypeDescription
is_protectedbooleantrue for built-in roles that cannot be deleted
permissionsstring[]List of permission keys assigned to the role
users_countintegerNumber of users currently assigned this role

Create a Role

Create a custom role with a specific set of permissions.

bash
curl -X POST https://moon-erp.test/api/core/roles \
  -H "Accept: application/json" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "supervisor",
    "permissions": ["core.dashboard.view", "core.users.view"]
  }'
dart
final response = await http.post(
  Uri.parse('https://moon-erp.test/api/core/roles'),
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer $token',
    'Content-Type': 'application/json',
  },
  body: jsonEncode({
    'name': 'supervisor',
    'permissions': ['core.dashboard.view', 'core.users.view'],
  }),
);

Request Fields:

FieldTypeRequiredDescription
namestringYesUnique role name
permissionsstring[]YesAt least one valid permission key

Response (201):

json
{
  "data": {
    "id": 7,
    "name": "supervisor",
    "guard_name": "web",
    "is_protected": false,
    "permissions": ["core.dashboard.view", "core.users.view"],
    "users_count": 0,
    "created_at": "2026-02-21T00:00:00.000000Z"
  }
}

Update a Role

Update a role's name and/or permissions. Both fields are optional — send only what you want to change.

bash
curl -X PUT https://moon-erp.test/api/core/roles/7 \
  -H "Accept: application/json" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "senior-supervisor",
    "permissions": ["core.dashboard.view", "core.users.view", "core.users.create"]
  }'
dart
final response = await http.put(
  Uri.parse('https://moon-erp.test/api/core/roles/7'),
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer $token',
    'Content-Type': 'application/json',
  },
  body: jsonEncode({
    'name': 'senior-supervisor',
    'permissions': ['core.dashboard.view', 'core.users.view', 'core.users.create'],
  }),
);

Request Fields:

FieldTypeRequiredDescription
namestringNoNew unique role name
permissionsstring[]NoReplaces all existing permissions (at least one)

Delete a Role

Delete a custom role. Built-in roles and roles assigned to users cannot be deleted.

bash
curl -X DELETE https://moon-erp.test/api/core/roles/7 \
  -H "Accept: application/json" \
  -H "Authorization: Bearer {token}"
dart
final response = await http.delete(
  Uri.parse('https://moon-erp.test/api/core/roles/7'),
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer $token',
  },
);

Response (200):

json
{
  "message": "Deleted"
}

Error Responses:

  • 422 Cannot delete a built-in role — the role is one of the 6 protected roles
  • 422 Cannot delete a role that is assigned to users — reassign or remove users first

List Permissions

Retrieve all available permissions grouped by module. Use this to build a permission picker UI when creating or editing roles.

bash
curl -X GET https://moon-erp.test/api/core/permissions \
  -H "Accept: application/json" \
  -H "Authorization: Bearer {token}"
dart
final response = await http.get(
  Uri.parse('https://moon-erp.test/api/core/permissions'),
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer $token',
  },
);

final data = jsonDecode(response.body)['data'] as Map<String, dynamic>;
// data['core'] -> list of core permissions
// data['accounting'] -> list of accounting permissions

Response:

json
{
  "data": {
    "core": [
      { "key": "core.company.view", "label": "core.company.view" },
      { "key": "core.dashboard.view", "label": "core.dashboard.view" },
      { "key": "core.users.view", "label": "core.users.view" },
      { "key": "core.users.create", "label": "core.users.create" }
    ],
    "accounting": [
      { "key": "accounting.accounts.view", "label": "accounting.accounts.view" },
      { "key": "accounting.journals.view", "label": "accounting.journals.view" }
    ]
  }
}

How Roles Work

Assignment

Roles are assigned during user creation or update via the role field on the users API:

json
{
  "name": "Fatima Hassan",
  "name_ar": "فاطمة حسن",
  "email": "fatima@moon-trading.com",
  "password": "secret1234",
  "password_confirmation": "secret1234",
  "role": "accountant"
}

When updating a user, providing a role value replaces the user's current roles with the new one.

Checking Roles

Roles are included in the user profile response from both the auth and user management endpoints:

json
{
  "data": {
    "id": 3,
    "name": "Fatima Hassan",
    "roles": ["accountant"],
    "permissions": ["accounting.journals.view", "accounting.journals.create"]
  }
}

Permission Format

Permissions follow the convention {module}.{resource}.{action}:

ExampleMeaning
core.users.viewCan view users
core.users.createCan create users
core.settings.manageCan manage settings
accounting.journals.viewCan view journal entries
accounting.journals.createCan create journal entries
accounting.journals.approveCan approve journal entries

Backend Enforcement

Every API controller enforces permissions server-side using Laravel's HasMiddleware interface. This means even if the frontend hides a menu item, the API will reject unauthorized requests with a 403 Forbidden response.

How It Works

Each controller declares which permissions are needed for which actions:

php
class ProductController extends Controller implements HasMiddleware
{
    public static function middleware(): array
    {
        return [
            new Middleware('permission:core.products.view', only: ['index', 'show']),
            new Middleware('permission:core.products.create', only: ['store']),
            new Middleware('permission:core.products.update', only: ['update']),
            new Middleware('permission:core.products.delete', only: ['destroy']),
        ];
    }
}

Permission Coverage

All ~55 API controllers across all modules enforce permissions:

ModuleControllersPermissions
Core21 controllerscompany, branches, users, roles, settings, sequences, products, partners, categories, units, attachments, notifications, admin, dashboard
Accounting27 controllersaccounts, fiscal years, cost centers, currencies, exchange rates, journal entries (view/create/approve/post/cancel/reverse), tax rates, bank accounts, checks, petty cash, transfers, templates, budgets, allocation rules, fixed assets, reconciliation, AR/AP, opening balances, year-end closing, reports
Support8 controllerstasks, comments, labels, attachments, time entries, epics, sprints, kanban columns

Special Actions

Some controllers define granular permissions beyond basic CRUD:

PermissionMeaning
accounting.journal-entries.approveCan approve draft journal entries
accounting.journal-entries.postCan post approved entries to the ledger
accounting.journal-entries.reverseCan reverse posted entries
accounting.checks-issued.change-statusCan deliver, cash, or cancel issued checks
accounting.fixed-assets.depreciateCan run depreciation on fixed assets
accounting.year-end-closing.executeCan execute or reopen year-end closing
core.admin.manageCan reset database and import test data

Authorization Flow

Error Responses

When a user lacks the required authorization:

401 Unauthorized -- no valid bearer token provided:

json
{
  "message": "Unauthenticated."
}

403 Forbidden -- user does not have the required role or permission:

json
{
  "message": "Unauthorized"
}

Retrieving Current User Permissions

The GET /api/auth/me endpoint returns the authenticated user's roles and permissions. Use this on the client side to conditionally show or hide UI elements.

bash
curl -X GET https://moon-erp.test/api/auth/me \
  -H "Accept: application/json" \
  -H "Authorization: Bearer {token}"
dart
final response = await http.get(
  Uri.parse('https://moon-erp.test/api/auth/me'),
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer $token',
  },
);

final user = jsonDecode(response.body)['data'];
final roles = List<String>.from(user['roles']);
final permissions = List<String>.from(user['permissions']);

// Example: check if user can create journal entries
if (permissions.contains('accounting.journals.create')) {
  // Show "New Journal Entry" button
}

Business Rules

  • Built-in roles — the 6 default roles (owner, admin, manager, accountant, cashier, employee) are marked as is_protected: true and cannot be deleted.
  • Roles with users — a role that has users assigned to it cannot be deleted. Reassign users first.
  • Default role on registration — new users registered via POST /api/auth/register are automatically assigned the employee role.
  • Single role per user — the users API assigns one role at a time via the role field. The underlying system supports multiple roles, but the API simplifies this to a single role per update.
  • Role changes replace existing roles — when updating a user's role, the previous roles are removed and replaced with the new one using syncRoles().
  • Permissions are inherited from roles — individual direct permissions can also be assigned, but the primary mechanism is role-based.
  • Permission sync on update — when updating a role's permissions, the provided list replaces all existing permissions (not a merge).

Moon ERP API Documentation