Users and Roles

Accounts, roles, and the dot-notation permissions that guard routes and templates.

On this page

Users

A user is a folder at site/data/users/{username}/ with an _index.json and a _data.json, the same split as a page. The role and hashed password live in _data.json.

phlat user:create adam --role=admin
phlat user:role adam editor
phlat user:password adam

A built-in guest user stands in for anyone not signed in. It holds the guest role, which grants nothing by default.

Roles

A role is a folder with an _index.json listing permissions. Site roles are checked before the framework's:

phlat/roles/admin/_index.json    ← ships with the framework
phlat/roles/guest/_index.json
site/roles/editor/_index.json    ← yours
{
    "locked": false,
    "permissions": {
        "admin": { "access": true },
        "pages": { "edit": true, "publish": true }
    }
}

Permissions are nested and checked with dot notation: pages.publish looks up permissions.pages.publish. A * key at any level is a wildcard for everything not listed there; the admin role is { "*": true }. A missing permission is denied.

A signed-out visitor is checked against the guest role, so can() and requirePermission() work the same for everyone. To grant guests a permission, add site/roles/guest/_index.json. A site role replaces the framework's role of the same name.

Checking permissions

requirePermission() guards a route. It redirects to login when nobody is signed in and throws ForbiddenException (403) when the user lacks the permission:

$page->router->post('publish', function () use ($app, $page) {
    $app->session->requirePermission('pages.publish');
    // ...
});

requireLogin() accepts any signed-in user. can() checks without stopping the request:

if ($app->session->can('pages.edit')) { /* ... */ }

In Latte:

{if permission('pages.edit')}
    <a href="{$page->url()}/edit">Edit</a>
{/if}

Put checks in a before() hook to guard a whole section (see Routing). To restrict a page and everything under it to signed-in users or specific roles, set its access instead. See Publishing and access.

Profile fields

Extra user fields are declared in roles/_index.json under fields, like a view declares page fields. Each name refers to a field in site/fields/, or extends a core one ("avatar": { "extends": "@phlat/avatar" }), and values read like page fields.

{
    "fields": {
        "password": { "required": true },
        "avatar": {},
        "jobTitle": {}
    }
}

Rate limiting

$app->session->throttle($key, $max, $window = 300) records one attempt for a key and returns true once more than $max attempts have been recorded within the window. Attempts are counted in SQLite, so concurrent requests each get their own number. Login uses it to slow repeated failures; use it for any form.