Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 109 additions & 1 deletion versions/4.x/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Authentications
description: Doppar authentications page
meta:
- name: keywords
content: authentications
content: authentications, logout other devices, browser sessions, auth.session middleware, remember me, two-factor authentication
---

## Authentication
Expand Down Expand Up @@ -175,6 +175,113 @@ class LoginController extends Controller
}
```

## Logging Out Other Devices
A user who suspects their account is in use somewhere else, or who just changed their password, can sign out of every other browser and device while staying signed in on the current one. Ask for their current password to confirm, then call `logoutOtherDevices()`:

```php
<?php

namespace App\Http\Controllers;

use Phaseolies\Http\Request;
use Phaseolies\Support\Facades\Auth;

class SecurityController extends Controller
{
public function logoutOtherDevices(Request $request)
{
if (!Auth::logoutOtherDevices($request->password)) {
return back()->withErrors(['password' => 'The password is incorrect.']);
}

return back()->with('status', 'You have been signed out of your other devices.');
}
}
```

`logoutOtherDevices()` returns `false` when no user is signed in or the password is wrong, and changes nothing in that case. Otherwise it:

- **Re-hashes the password.** The password itself does not change, but every other session was signed in with the old hash, so it is no longer accepted.
- **Replaces the remember-me token.** Other devices cannot sign back in from their remember-me cookie. If the current device was remembered, it gets a new cookie and stays remembered.
- **Keeps the current device signed in.**
- **Deletes the other sessions immediately** when you use the [database session driver](/versions/4.x/session#database-driver), which records the user of each session.

### Checking Sessions on Every Request
With the `file` and `cookie` drivers, the other devices' sessions still exist after `logoutOtherDevices()`. They are signed out the next time they make a request, by the `AuthenticateSession` middleware. Register it in the `web` middleware group of `src/Http/Gateway.php`, so it runs before `auth`:

```php
public $middlewareGroups = [
'web' => [
\Phaseolies\Middleware\CsrfTokenMiddleware::class,
\Phaseolies\Middleware\AuthenticateSession::class,
],
'api' => [],
];
```

The middleware compares the password the session was signed in with to the user's current password. A session that no longer matches is signed out, and the request carries on as a guest, so the `auth` middleware of a protected route sends it to the login page.

This also signs a user out of their other devices whenever their password changes, for example after a password reset. The device that changed the password stays signed in, as long as it changed the password of the signed-in user (`auth()->user()`). If you change it on another instance of the model, keep the current device signed in with:

```php
auth()->refreshSessionFingerprint();
```

> **Note:** The middleware is optional with the `database` driver, which deletes the other sessions immediately. Registering it anyway also covers password changes made outside `logoutOtherDevices()`.

Sessions created before the middleware was registered are accepted once and then checked from their next request on, so registering it does not sign anyone out.

### Listing a User's Devices
With the `database` driver, `sessions()` returns the devices the signed-in user is currently signed in on, most recently active first:

```php
$sessions = auth()->sessions();

// [
// ['ip_address' => '203.0.113.9', 'user_agent' => 'Mozilla/5.0 ...', 'last_activity' => 1730700000, 'is_current' => true],
// ['ip_address' => '198.51.100.4', 'user_agent' => 'Mozilla/5.0 ...', 'last_activity' => 1730690000, 'is_current' => false],
// ]
```

Session IDs are never returned, because a session ID is enough to act as the user. With other drivers, `sessions()` returns an empty array.

```html
#foreach (auth()->sessions() as $session)
<li>
[[ $session['user_agent'] ?? 'Unknown device' ]] · [[ $session['ip_address'] ]]
#if ($session['is_current']) (this device) #endif
</li>
#endforeach
```

### Other Actors
The database driver records the user of the **default** actor only. For any other actor, `logoutOtherDevices()` still re-hashes the password and replaces the remember-me token, and the `AuthenticateSession` middleware signs the other devices out, but no session is deleted immediately and `sessions()` is empty. Apply the middleware to such an actor's routes with its name:

```php
#[Middleware(['auth.session:admin'])]
```

after registering the alias in `src/Http/Gateway.php`:

```php
'auth.session' => \Phaseolies\Middleware\AuthenticateSession::class,
```

### Custom Password and Remember Token Columns
`logoutOtherDevices()` writes the password and remember token columns directly, so it works even when they are not in the model's `$creatable` list, and model hooks do not run. If your users table names them differently, override these methods on your model:

```php
public function getAuthPasswordName(): string
{
return 'password_hash';
}

public function getRememberTokenName(): string
{
return 'remember_me_token';
}
```

## Get Authenticated User Data
To get the current authenticated user data, Doppar has `Auth::user()` method and `auth()` helper. Simply call

Expand Down Expand Up @@ -322,6 +429,7 @@ Each actor writes its state to separate, namespaced session keys and cookies so
| Remember-me cookie | `remember_doppar_web_<hash>` | `remember_doppar_admin_<hash>` |
| 2FA pending user | `2fa_web_user_id` | `2fa_admin_user_id` |
| 2FA remember flag | `2fa_web_remember` | `2fa_admin_remember` |
| Password fingerprint | `password_hash_web` | `password_hash_admin` |

### Logging Out of a Specific Actor
Log out of the admin actor only — web actor session is untouched
Expand Down
2 changes: 1 addition & 1 deletion versions/4.x/csrf-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ A front end that never loads such a page therefore has no cookie on its first re
Requests that are [stateless](/versions/4.x/session#stateless-routes) have no stored session to tie a token to, so Doppar does not issue the `XSRF-TOKEN` cookie for them and they cannot pass CSRF verification. Keep token authenticated endpoints, such as a JSON API, out of the `web` middleware group, or list them in `setRelaxablePaths` as shown below.

## Ignoring CSRF Protection
To exclude certain routes from CSRF token verification (useful for APIs, webhooks, or external integrations), update the `setRelaxablePaths` array in the `src/runtime/app.php` file.
To exclude certain routes from CSRF token verification (useful for APIs, webhooks, or external integrations), update the `setRelaxablePaths` array in the `src/runtime/app.php` file. Patterns ignore case, the leading slash is optional, and `*` matches anything, exactly as they do for the [stateless](/versions/4.x/session#stateless-routes) paths of the session.

Locate the section where the application is being built and modify it like so:

Expand Down
20 changes: 16 additions & 4 deletions versions/4.x/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,13 @@ Equivalent to the above, using the `input()` method
$request->input('name', 'default');
```

Called without a key, `input()` returns the complete input array, the same as `all()`:
```php
$request->input();
```

`input()` and `has()` always read the current request data, so values you change with `merge()` or directly on the `$request->request` bag are visible right away.

`get()` is a lower-level lookup. It checks route parameters first, then the query string, then the request body, and returns the first match. Prefer `input()` when you only want the request input.
```php
$request->get('name', 'default');
Expand Down Expand Up @@ -731,6 +738,8 @@ $request->host();
$request->scheme();
```

`scheme()` returns the same value as `getScheme()`. Behind a [trusted proxy](/versions/4.x/trust-proxies), it honors the `X-Forwarded-Proto` header, so a request that reaches the proxy over HTTPS reports `https` even if the proxy forwards it to your application over plain HTTP.

## Retrieving the Request Method

The `method()` method returns the HTTP verb in lowercase, while `getMethod` returns it in uppercase. You may use the `isGet`, `isPost`, `isPut`, `isPatch`, `isDelete` and `isHead` methods to verify that the HTTP verb matches:
Expand Down Expand Up @@ -782,19 +791,22 @@ $token = $request->bearerToken();

## Request IP Address

The `ip` method may be used to retrieve the IP address of the client that made the request to your application
The `ip` method may be used to retrieve the IP address of the client that made the request to your application. It is an alias of `getClientIp()`.

```php
$ipAddress = $request->ip();
```

To retrieve an array of IP addresses — including all client IPs forwarded by proxies — you can use the `ips()` method on the `Phaseolies\Http\Request` instance. This method returns all available IPs in order, with the original client IP listed at the end of the array.
To retrieve an array of IP addresses — including all client IPs forwarded by proxies — you can use the `ips()` method on the `Phaseolies\Http\Request` instance. It is equivalent to `getClientIps()`: trusted proxy addresses are removed, and the list is ordered from most to least trusted. The first element is the closest address that isn't a trusted proxy (the value `ip()` returns), and the last is the originating client as reported by the forwarding headers.

```php
$ipAddresses = $request->ips();
// Behind a trusted proxy, with X-Forwarded-For: 203.0.113.1, 198.51.100.2
$ipAddresses = $request->ips(); // ["198.51.100.2", "203.0.113.1"]
```

This is especially useful when your application is behind a load balancer or proxy and you want to trace the full route of the request
Forwarding headers such as `X-Forwarded-For` and `Forwarded` are only read when the request comes from a [trusted proxy](/versions/4.x/trust-proxies). For any other request, both methods return the connecting address (`REMOTE_ADDR`), so clients cannot spoof their IP by sending these headers themselves.

If a trusted proxy sends a `Forwarded` and an `X-Forwarded-*` header that disagree, the request is rejected with a `400 Bad Request` (`ConflictingHeadersException`). Configure your proxy to send only one of them, or trust only the one it sets.

## Content Negotiation

Expand Down
16 changes: 16 additions & 0 deletions versions/4.x/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ Response:
}
```

The same rules apply whether you return the value from a route or pass it to `response()`:

| Value | Sent as |
| --- | --- |
| Array, `stdClass`, `ArrayObject` | JSON |
| Entity model, collection, query builder | JSON (a query builder is executed first) |
| Object implementing `JsonSerializable` | JSON, even if it also implements `Stringable` |
| Object implementing only `Stringable` | Its string value, as a normal response |
| Any other object | JSON (via `toArray()` when the object has one) |
| An existing `Response` instance | The same instance. `response($response, 503, ['Retry-After' => '10'])` applies the status and headers you pass. If you pass no status (or `200`), the response keeps its own |

#### Returning Collections

Doppar's collections can be returned directly from routes. They're automatically converted to JSON:
Expand Down Expand Up @@ -328,6 +339,9 @@ response()->redirect('/new-location', 301);
// 302 Found (Temporary redirect)
response()->redirect('/temporary-location', 302);

// response()->redirect() returns a RedirectResponse, so you can chain flash data
response()->redirect('/dashboard')->with('success', 'Saved!');

// 304 Not Modified (for caching)
response()->setNotModified();
```
Expand Down Expand Up @@ -847,6 +861,8 @@ public function streamReportDownload()

Streaming data to the client as it is generated can greatly reduce memory usage and enhance performance, particularly for extremely large responses.

The stream callback is not run for `HEAD` requests or `204`/`304` responses, since those must not have a body. A streamed response has no buffered body, so calling `setBody()` with a string on it throws a `LogicException`. The same applies to file responses.

#### Basic Streaming

Stream content to the client as it's generated to reduce memory usage:
Expand Down
28 changes: 28 additions & 0 deletions versions/4.x/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,34 @@ Doppar supports the following HTTP methods for defining routes:
| **HEAD** | Same as GET but returns only headers. Useful for checking existence or metadata. |
| **OPTIONS** | Describes available communication options. Often used for CORS preflight checks. | |

### HEAD Requests

You don't need to register HEAD routes. A `HEAD` request is answered by the `GET` route for the same path, with the same middleware, so a protected page still redirects or rejects as it would for `GET`. The response contains only the headers. No body is sent, and a streamed response's callback is not run.

If you register an explicit `HEAD` route, it takes precedence over the `GET` route for that path, along with its own middleware:

```php
use Phaseolies\Support\Router\Attributes\Route;

class ReportController extends Controller
{
#[Route(uri: '/report')]
public function show()
{
// ...
}

// Optional: a cheaper handler for HEAD requests to the same path
#[Route(uri: '/report', methods: ['HEAD'])]
public function headers()
{
// ...
}
}
```

`HEAD` never falls back to `POST`, `PUT`, `PATCH` or `DELETE` routes.

## Route Caching
Doppar supports route caching for improved performance in production environments. Route caching is controlled by the `APP_ROUTE_CACHE` environment variable: Set to true to enable route caching (recommended for production). Set to false to disable (default for development) Route cache files are stored in: `storage/framework/cache/`.
### Cache Routes
Expand Down
Loading