SiteWeaver_CMS/database/seeds/RolesAndPermissionsSeed.php

241 lines
9.1 KiB
PHP

<?php
/**
* RolesAndPermissionsSeed.php
*
* Author: Funky Waddle
* Date: 2026-09-06
*
* Purpose: Seeds the default roles (Admin, Editor, Author, User), the
* baseline permission set, and their `role_permission` assignments, adapted
* from `data_model.example_seed_data_for_roles` in APP.yaml. Since `roles`/
* `permissions`/`users` all carry a NOT NULL `created_by` FK back to `users`,
* this also bootstraps the very first (primary) Admin user account if the
* `users` table is empty - the "primary Admin account" protected from edit/
* delete per `auth_requirements`. Its temporary password should be changed
* immediately after first login.
*/
declare(strict_types=1);
use Phinx\Seed\AbstractSeed;
final class RolesAndPermissionsSeed extends AbstractSeed
{
public function getDependencies(): array
{
return [];
}
public function run(): void
{
$adminUserId = $this->ensurePrimaryAdminUser();
$roleIds = $this->ensureRoles($adminUserId);
$permissionIds = $this->ensurePermissions($adminUserId);
$this->ensureUserHasRole($adminUserId, $roleIds['Admin'], $adminUserId);
$this->assignPermissions($adminUserId, $roleIds, $permissionIds, 'Admin', array_keys($permissionIds));
$this->assignPermissions($adminUserId, $roleIds, $permissionIds, 'Editor', [
'publish:page', 'unpublish:page', 'edit:page', 'delete:page',
'view:draft', 'edit:draft',
'view:blog_post', 'edit:blog_post', 'delete:blog_post', 'publish:blog_post',
'view:media', 'edit:media', 'delete:media',
]);
$this->assignPermissions($adminUserId, $roleIds, $permissionIds, 'Author', [
'publish:page', 'edit:page', 'edit:draft', 'publish:draft',
'view:blog_post', 'edit:blog_post', 'publish:blog_post',
'view:media', 'edit:media',
]);
// 'User' (public commenter role) intentionally gets no backend permissions.
}
private function ensurePrimaryAdminUser(): int
{
$existing = $this->fetchRow('SELECT id FROM users ORDER BY id ASC LIMIT 1');
if ($existing !== false) {
return (int) $existing['id'];
}
$now = date('Y-m-d H:i:s');
// `created_by` is a NOT NULL self-referencing FK; the very first user
// in the system has no other creator, so the row is inserted with a
// placeholder and immediately corrected to reference itself.
$this->execute(
'INSERT INTO users (username, password, f_name, l_name, email, failed_login_attempts, created_at, created_by)
VALUES (?, ?, ?, ?, ?, 0, ?, 0)',
[
'admin',
password_hash(bin2hex(random_bytes(8)), PASSWORD_DEFAULT),
'Site',
'Admin',
'admin@siteweaver.local',
$now,
],
);
$adminUserId = (int) $this->getAdapter()->getConnection()->lastInsertId();
$this->execute('UPDATE users SET created_by = ? WHERE id = ?', [$adminUserId, $adminUserId]);
return $adminUserId;
}
private function ensureUserHasRole(int $userId, int $roleId, int $adminUserId): void
{
$exists = $this->fetchRow("SELECT 1 FROM user_role WHERE user_id = $userId AND role_id = $roleId");
if ($exists !== false) {
return;
}
$this->execute(
'INSERT INTO user_role (user_id, role_id, created_at, created_by) VALUES (?, ?, ?, ?)',
[$userId, $roleId, date('Y-m-d H:i:s'), $adminUserId],
);
}
/**
* @return array<string, int> role name => id
*/
private function ensureRoles(int $adminUserId): array
{
$definitions = [
'Admin' => 'Full access to all CMS features and settings.',
'Editor' => 'Can create, edit, and delete content, but cannot change site settings.',
'Author' => 'Can create and edit their own content; can view drafts.',
'User' => 'Basic frontend read-only access to published content. No backend admin access. Can comment on Blog Posts.',
];
$ids = [];
foreach ($definitions as $name => $description) {
$row = $this->fetchRow("SELECT id FROM roles WHERE name = '" . $name . "'");
if ($row !== false) {
$ids[$name] = (int) $row['id'];
continue;
}
$this->execute(
'INSERT INTO roles (name, description, created_at, created_by) VALUES (?, ?, ?, ?)',
[$name, $description, date('Y-m-d H:i:s'), $adminUserId],
);
$ids[$name] = (int) $this->getAdapter()->getConnection()->lastInsertId();
}
return $ids;
}
/**
* @return array<string, int> permission name => id
*/
private function ensurePermissions(int $adminUserId): array
{
// Common CMS permissions (per `data_model.example_seed_data_for_roles`)
// plus the granular View/Edit/Delete-per-resource permissions called
// for by `auth_requirements`'s "Granular Permissions" note, for every
// resource named there: Page, Plugin, Theme, Blog Post, User.
$definitions = [
['manage:site', null, 'configure', 'Change site-wide settings (e.g., logo, navigation).'],
['publish:page', 'page', 'publish', 'Publish a page to the public site.'],
['unpublish:page', 'page', 'unpublish', 'Remove a page from public view.'],
['edit:page', 'page', 'edit', 'Modify an existing page.'],
['delete:page', 'page', 'delete', 'Permanently delete a page.'],
['view:draft', 'draft', 'view', 'Read unpublished drafts.'],
['edit:draft', 'draft', 'edit', 'Edit an unpublished draft.'],
['publish:draft', 'draft', 'publish', 'Publish a draft to the public site.'],
['manage:user', null, 'admin', 'Create, edit, disable, or delete user accounts.'],
['view:user', 'user', 'view', 'View user accounts.'],
['edit:user', 'user', 'edit', 'Edit user accounts.'],
['delete:user', 'user', 'delete', 'Delete user accounts.'],
['manage:role', null, 'admin', 'Create, edit, or delete roles and their permissions.'],
['view:plugin', 'plugin', 'view', 'View installed plugins.'],
['edit:plugin', 'plugin', 'edit', 'Configure/activate/deactivate plugins.'],
['delete:plugin', 'plugin', 'delete', 'Uninstall/delete plugins.'],
['view:theme', 'theme', 'view', 'View installed themes.'],
['edit:theme', 'theme', 'edit', 'Edit/activate theme files and settings.'],
['delete:theme', 'theme', 'delete', 'Delete themes.'],
['view:blog_post', 'blog_post', 'view', 'View blog posts (including drafts by others, if granted).'],
['edit:blog_post', 'blog_post', 'edit', 'Create/edit blog posts.'],
['delete:blog_post', 'blog_post', 'delete', "Delete blog posts, including the author's own published posts."],
['publish:blog_post', 'blog_post', 'publish', 'Publish a blog post to the public site.'],
['view:media', 'media', 'view', 'View media library items.'],
['edit:media', 'media', 'edit', 'Upload/edit media library items.'],
['delete:media', 'media', 'delete', 'Delete media library items.'],
];
$ids = [];
foreach ($definitions as [$name, $type, $action, $description]) {
$row = $this->fetchRow("SELECT id FROM permissions WHERE name = '" . $name . "'");
if ($row !== false) {
$ids[$name] = (int) $row['id'];
continue;
}
$this->execute(
'INSERT INTO permissions (name, type, action, description, created_at, created_by) VALUES (?, ?, ?, ?, ?, ?)',
[$name, $type, $action, $description, date('Y-m-d H:i:s'), $adminUserId],
);
$ids[$name] = (int) $this->getAdapter()->getConnection()->lastInsertId();
}
return $ids;
}
/**
* @param array<string, int> $roleIds
* @param array<string, int> $permissionIds
* @param string[] $permissionNames
*/
private function assignPermissions(
int $adminUserId,
array $roleIds,
array $permissionIds,
string $roleName,
array $permissionNames,
): void {
if (!isset($roleIds[$roleName])) {
return;
}
$roleId = $roleIds[$roleName];
foreach ($permissionNames as $permissionName) {
if (!isset($permissionIds[$permissionName])) {
continue;
}
$permissionId = $permissionIds[$permissionName];
$exists = $this->fetchRow(
"SELECT 1 FROM role_permission WHERE role_id = $roleId AND permission_id = $permissionId",
);
if ($exists !== false) {
continue;
}
$this->execute(
'INSERT INTO role_permission (role_id, permission_id, created_at, created_by) VALUES (?, ?, ?, ?)',
[$roleId, $permissionId, date('Y-m-d H:i:s'), $adminUserId],
);
}
}
}