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
| Method | Endpoint | Description |
|---|---|---|
GET | /api/core/roles | List all roles |
POST | /api/core/roles | Create 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/permissions | List all permissions grouped by module |
Built-in Roles
The system ships with the following built-in (protected) roles that cannot be deleted:
| Role | Description |
|---|---|
owner | Company owner with full access |
admin | Full access to all modules and features |
manager | Managerial access across modules |
accountant | Access to accounting, journal entries, reports |
cashier | POS and cash register access |
employee | Basic access, assigned by default on registration |
List Roles
Retrieve all roles with their permissions, user count, and protection status.
curl -X GET https://moon-erp.test/api/core/roles \
-H "Accept: application/json" \
-H "Authorization: Bearer {token}"final response = await http.get(
Uri.parse('https://moon-erp.test/api/core/roles'),
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);Response:
{
"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"
}
]
}| Field | Type | Description |
|---|---|---|
is_protected | boolean | true for built-in roles that cannot be deleted |
permissions | string[] | List of permission keys assigned to the role |
users_count | integer | Number of users currently assigned this role |
Create a Role
Create a custom role with a specific set of permissions.
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"]
}'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:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique role name |
permissions | string[] | Yes | At least one valid permission key |
Response (201):
{
"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.
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"]
}'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:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | New unique role name |
permissions | string[] | No | Replaces all existing permissions (at least one) |
Delete a Role
Delete a custom role. Built-in roles and roles assigned to users cannot be deleted.
curl -X DELETE https://moon-erp.test/api/core/roles/7 \
-H "Accept: application/json" \
-H "Authorization: Bearer {token}"final response = await http.delete(
Uri.parse('https://moon-erp.test/api/core/roles/7'),
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);Response (200):
{
"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.
curl -X GET https://moon-erp.test/api/core/permissions \
-H "Accept: application/json" \
-H "Authorization: Bearer {token}"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 permissionsResponse:
{
"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:
{
"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:
{
"data": {
"id": 3,
"name": "Fatima Hassan",
"roles": ["accountant"],
"permissions": ["accounting.journals.view", "accounting.journals.create"]
}
}Permission Format
Permissions follow the convention {module}.{resource}.{action}:
| Example | Meaning |
|---|---|
core.users.view | Can view users |
core.users.create | Can create users |
core.settings.manage | Can manage settings |
accounting.journals.view | Can view journal entries |
accounting.journals.create | Can create journal entries |
accounting.journals.approve | Can 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:
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:
| Module | Controllers | Permissions |
|---|---|---|
| Core | 21 controllers | company, branches, users, roles, settings, sequences, products, partners, categories, units, attachments, notifications, admin, dashboard |
| Accounting | 27 controllers | accounts, 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 |
| Support | 8 controllers | tasks, comments, labels, attachments, time entries, epics, sprints, kanban columns |
Special Actions
Some controllers define granular permissions beyond basic CRUD:
| Permission | Meaning |
|---|---|
accounting.journal-entries.approve | Can approve draft journal entries |
accounting.journal-entries.post | Can post approved entries to the ledger |
accounting.journal-entries.reverse | Can reverse posted entries |
accounting.checks-issued.change-status | Can deliver, cash, or cancel issued checks |
accounting.fixed-assets.depreciate | Can run depreciation on fixed assets |
accounting.year-end-closing.execute | Can execute or reopen year-end closing |
core.admin.manage | Can reset database and import test data |
Authorization Flow
Error Responses
When a user lacks the required authorization:
401 Unauthorized -- no valid bearer token provided:
{
"message": "Unauthenticated."
}403 Forbidden -- user does not have the required role or permission:
{
"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.
curl -X GET https://moon-erp.test/api/auth/me \
-H "Accept: application/json" \
-H "Authorization: Bearer {token}"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 asis_protected: trueand 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/registerare automatically assigned theemployeerole. - Single role per user — the users API assigns one role at a time via the
rolefield. 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).