# Authorization

Magic's authorisation system lets you define and check user abilities via the `Gate` facade, with support for policies, super-admin bypasses, and declarative UI widgets.

- [Introduction](#introduction)
- [Defining Abilities](#defining-abilities)
- [Checking Abilities](#checking-abilities)
- [Super Admin Bypass](#super-admin-bypass)
- [Policies](#policies)
- [Gate Utility Methods](#gate-utility-methods)
- [UI Integration](#ui-integration)
    - [MagicCan Widget](#magiccan-widget)
    - [MagicCannot Widget](#magiccannot-widget)
- [GateServiceProvider](#gateserviceprovider)
- [Generating Policies](#generating-policies)
- [Related Documentation](#related-documentation)

## Introduction

In addition to authentication, Magic provides a simple way to authorise user actions against a given resource. Like Laravel, authorisation logic is defined using the `Gate` facade and may be checked anywhere in your application.

```dart
// Define abilities (in provider)
Gate.define('update-post', (user, post) => user.id == post.userId);

// Check abilities (in code)
if (Gate.allows('update-post', post)) {
  showEditButton();
}

// Declarative (in UI)
MagicCan(
  ability: 'update-post',
  arguments: post,
  child: EditButton(),
)
```

## Defining Abilities

Register abilities using `Gate.define()`. The callback receives the authenticated user as the first argument and optional model/data as the second:

```dart
// Simple ability (no model)
Gate.define('view-dashboard', (user, _) => user.isActive);

// With model
Gate.define('update-post', (user, post) => user.id == post.userId);

// Complex logic
Gate.define('delete-post', (user, post) {
  return user.isAdmin || user.id == post.userId;
});

// Multiple conditions
Gate.define('manage-team', (user, team) {
  return team.ownerId == user.id || 
         team.admins.contains(user.id);
});
```

## Checking Abilities

### In Controllers

```dart
class PostController extends MagicController {
  Future<void> update(String id, Map<String, dynamic> data) async {
    final post = await Post.find(id);
    if (post == null) return;
    
    if (Gate.denies('update-post', post)) {
      Magic.error('Forbidden', 'You cannot edit this post.');
      return;
    }
    
    post.fill(data);
    await post.save();
    Magic.success('Success', 'Post updated!');
  }
}
```

### Available Methods

```dart
// Check if allowed
if (Gate.allows('update-post', post)) {
  // User can update
}

// Check if denied
if (Gate.denies('delete-post', post)) {
  // User cannot delete
}

// Alias for allows
if (Gate.check('view-admin')) {
  // Same as allows
}

// Any ability passes (short-circuits on first match)
if (Gate.allowsAny(['owner', 'admin'], project)) {
  // At least one ability allows access
}

// All abilities must pass (short-circuits on first failure)
if (Gate.allowsAll(['update', 'publish'], post)) {
  // Every ability allows access
}
```

A check is denied, never thrown, when nobody is signed in, when the ability was never defined, and when its callback throws.

### Inside Controllers

`MagicController` exposes an `authorize()` helper that delegates to the Gate and throws `AuthorizationException` when access is denied. This mirrors Laravel's `$this->authorize()` inside controller actions:

```dart
class MonitorController extends MagicController with MagicStateMixin<Monitor> {
  Future<void> update(String id, MonitorFormValues values) async {
    final monitor = await Monitor.find(id);
    authorize('update', monitor);
    // ... proceed with update, exception already thrown on denial
  }
}
```

Catch `AuthorizationException` at the boundary (route handler, form submit) to surface a 403 view or feedback toast.

## Super Admin Bypass

Use `Gate.before()` to register a global check that runs before all ability checks:

```dart
Gate.before((user, ability) {
  // Super admins bypass all checks
  if ((user as User).role == 'super_admin') return true;
  
  // Continue with normal ability check
  return null;
});
```

The callback receives the user as a `Model`, so cast it to your user class to read its own getters.

Return values:
- `true` → Allow immediately, skip ability check
- `false` → Deny immediately, skip ability check
- `null` → Continue with normal ability check

## Policies

Policies organise related authorisation logic into classes. This is the recommended approach for complex applications:

```dart
import 'package:magic/magic.dart';
import '../models/post.dart';

class PostPolicy extends Policy {
  @override
  void register() {
    Gate.define('view-post', view);
    Gate.define('create-post', create);
    Gate.define('update-post', update);
    Gate.define('delete-post', delete);
  }

  bool view(Authenticatable user, Post post) =>
      post.isPublished || user.id == post.userId;

  bool create(Authenticatable user, Post? post) =>
      (user as User).isActive;

  bool update(Authenticatable user, Post post) =>
      user.id == post.userId;

  bool delete(Authenticatable user, Post post) =>
      (user as User).isAdmin || user.id == post.userId;
}
```

### Registering Policies

Register policies in a service provider:

```dart
class AppGateServiceProvider extends GateServiceProvider {
  AppGateServiceProvider(super.app);

  @override
  Future<void> boot() async {
    await super.boot();

    // Register policies
    PostPolicy().register();
    CommentPolicy().register();
    TeamPolicy().register();
  }
}
```

## Gate Utility Methods

The Gate facade exposes several utility methods for inspecting registered abilities and resetting state.

```dart
// Check whether an ability has been defined
if (Gate.has('update-post')) {
  // Ability is registered
}

// Get a list of all defined ability names
final List<String> defined = Gate.abilities;

// Flush all abilities (useful in tests to start clean)
Gate.flush();
```

| Method | Returns | Description |
|:---|:---|:---|
| `Gate.has(ability)` | `bool` | Returns `true` if the named ability has been registered. |
| `Gate.abilities` | `List<String>` | All currently registered ability names. |
| `Gate.flush()` | `void` | Removes all registered abilities and before-callbacks. |

> [!TIP]
> `Gate.flush()` is intended for test teardown. Call it in `tearDown()` alongside `MagicApp.reset()` to ensure a clean gate state between tests.

## UI Integration

### MagicCan Widget

Conditionally render UI based on authorisation:

```dart
// Basic usage
MagicCan(
  ability: 'update-post',
  arguments: post,
  child: WButton(
    onTap: () => controller.edit(post),
    child: WText('Edit Post'),
  ),
)

// With placeholder for denied access
MagicCan(
  ability: 'view-admin-panel',
  child: AdminPanel(),
  placeholder: WText('Access Denied', className: 'text-red-500'),
)

// Without arguments
MagicCan(
  ability: 'view-dashboard',
  child: DashboardStats(),
)
```

### MagicCannot Widget

Show content only when user **lacks** an ability:

```dart
// Show upgrade prompt to non-premium users
MagicCannot(
  ability: 'access-premium',
  child: WDiv(
    className: 'p-4 bg-amber-500/10 rounded-lg',
    children: [
      WText('Upgrade to Premium', className: 'font-bold text-amber-500'),
      WText('Unlock all features with our premium plan.'),
    ],
  ),
)

// Show read-only indicator
MagicCannot(
  ability: 'edit-post',
  arguments: post,
  child: WText('Read Only', className: 'text-gray-500 italic'),
)
```

## GateServiceProvider

Create a dedicated provider for all authorisation logic:

```dart
import 'package:magic/magic.dart';

class AppGateServiceProvider extends GateServiceProvider {
  AppGateServiceProvider(super.app);

  @override
  Future<void> boot() async {
    await super.boot();

    // Super admin bypass
    Gate.before((user, ability) {
      if ((user as User).role == 'admin') return true;
      return null;
    });

    // Simple abilities
    Gate.define('view-dashboard', (user, _) => true);
    Gate.define('manage-settings', (user, _) => (user as User).isAdmin);

    // Register policies
    PostPolicy().register();
    TeamPolicy().register();
    MonitorPolicy().register();
  }
}
```

Register in `config/app.dart`:

```dart
'providers': [
  (app) => AppGateServiceProvider(app),
  // ... other providers
],
```

## Generating Policies

Use the [Magic CLI](https://anilcancakir.com/magic/docs/packages/magic-cli.md) to generate policy classes:

```bash
dart run magic:dowel make:policy Post
dart run magic:dowel make:policy PostPolicy          # Explicit naming
dart run magic:dowel make:policy Comment --model=Comment
```

### Options

| Option | Shortcut | Description |
|:---|:---|:---|
| `--model` | `-m` | The model that the policy applies to |

**Output:** Creates `lib/app/policies/<name>_policy.dart` with `view`, `create`, `update` and `delete` stubs, each returning `true` until you fill it in.

### Generated Policy Template

```dart
import 'package:magic/magic.dart';

// TODO: Import your model
// import '../models/post.dart';

/// PostPolicy
///
/// Defines authorization rules for [Post] resources.
class PostPolicy extends Policy {
  @override
  void register() {
    Gate.define('view-post', _view);
    Gate.define('create-post', _create);
    Gate.define('update-post', _update);
    Gate.define('delete-post', _delete);
  }

  /// Determine if the user can view the [Post].
  bool _view(Authenticatable user, dynamic arguments) {
    // TODO: Implement your authorization logic.
    return true;
  }

  // _create, _update and _delete follow the same shape.
}
```

> [!TIP]
> Use policies for model-specific authorisation and simple `Gate.define()` calls for general abilities like "view-dashboard" or "access-admin".

## Related Documentation

- [Authentication](https://anilcancakir.com/magic/docs/security/authentication.md)
- [Middleware](https://anilcancakir.com/magic/docs/basics/middleware.md)
- [Controllers](https://anilcancakir.com/magic/docs/basics/controllers.md)
- [Magic CLI](https://anilcancakir.com/magic/docs/packages/magic-cli.md)
