diff --git a/database/2018_07_17_170102_audit_log_struct.php b/database/2018_07_17_170102_audit_log_struct.php new file mode 100644 index 00000000..e65a1168 --- /dev/null +++ b/database/2018_07_17_170102_audit_log_struct.php @@ -0,0 +1,66 @@ +exec(' + ALTER TABLE `msz_permissions` + RENAME INDEX `user_id` TO `permissions_user_id_unique`, + RENAME INDEX `role_id` TO `permissions_role_id_unique`, + DROP FOREIGN KEY `role_id_foreign`, + DROP FOREIGN KEY `user_id_foreign`, + ADD CONSTRAINT `permissions_user_id_foreign` + FOREIGN KEY (`user_id`) + REFERENCES `msz_users` (`user_id`) + ON UPDATE CASCADE + ON DELETE CASCADE, + ADD CONSTRAINT `permissions_role_id_foreign` + FOREIGN KEY (`role_id`) + REFERENCES `msz_roles` (`role_id`) + ON UPDATE CASCADE + ON DELETE CASCADE + '); + + $conn->exec(" + CREATE TABLE `msz_audit_log` ( + `log_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` INT(10) UNSIGNED NULL DEFAULT NULL, + `log_action` VARCHAR(50) NOT NULL, + `log_params` TEXT NOT NULL, + `log_created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `log_ip` VARBINARY(16) NULL DEFAULT NULL, + PRIMARY KEY (`log_id`), + INDEX `audit_log_user_id_foreign` (`user_id`), + CONSTRAINT `audit_log_user_id_foreign` + FOREIGN KEY (`user_id`) + REFERENCES `msz_users` (`user_id`) + ON UPDATE CASCADE + ON DELETE CASCADE + ) + "); +} + +function migrate_down(PDO $conn): void +{ + $conn->exec('DROP TABLE `msz_audit_log`'); + + $conn->exec(' + ALTER TABLE `msz_permissions` + RENAME INDEX `permissions_user_id_unique` TO `user_id`, + RENAME INDEX `permissions_role_id_unique` TO `role_id`, + DROP FOREIGN KEY `permissions_user_id_foreign`, + DROP FOREIGN KEY `permissions_role_id_foreign`, + ADD CONSTRAINT `role_id_foreign` + FOREIGN KEY (`user_id`) + REFERENCES `msz_users` (`user_id`) + ON UPDATE CASCADE + ON DELETE CASCADE, + ADD CONSTRAINT `user_id_foreign` + FOREIGN KEY (`role_id`) + REFERENCES `msz_roles` (`role_id`) + ON UPDATE CASCADE + ON DELETE CASCADE + '); +} diff --git a/misuzu.php b/misuzu.php index 90c0b128..87c0d9bf 100644 --- a/misuzu.php +++ b/misuzu.php @@ -4,6 +4,7 @@ namespace Misuzu; date_default_timezone_set('UTC'); require_once __DIR__ . '/vendor/autoload.php'; +require_once __DIR__ . '/src/audit_log.php'; require_once __DIR__ . '/src/changelog.php'; require_once __DIR__ . '/src/colour.php'; require_once __DIR__ . '/src/comments.php'; diff --git a/public/manage/changelog.php b/public/manage/changelog.php index d93a8bec..bbf99895 100644 --- a/public/manage/changelog.php +++ b/public/manage/changelog.php @@ -113,8 +113,12 @@ switch ($_GET['v'] ?? null) { $postChange->execute(); if ($changeId < 1) { - header('Location: ?v=change&c=' . Database::lastInsertId()); + $changeId = Database::lastInsertId(); + audit_log('CHANGELOG_ENTRY_CREATE', $app->getUserId(), [$changeId]); + header('Location: ?v=change&c=' . $changeId); return; + } else { + audit_log('CHANGELOG_ENTRY_EDIT', $app->getUserId(), [$changeId]); } } @@ -122,7 +126,13 @@ switch ($_GET['v'] ?? null) { $addTag = Database::prepare('REPLACE INTO `msz_changelog_change_tags` VALUES (:change_id, :tag_id)'); $addTag->bindValue('change_id', $changeId); $addTag->bindValue('tag_id', $_POST['add_tag']); - $addTag->execute(); + + if ($addTag->execute()) { + audit_log('CHANGELOG_TAG_ADD', $app->getUserId(), [ + $changeId, + $_POST['add_tag'] + ]); + } } if (!empty($_POST['remove_tag']) && is_numeric($_POST['remove_tag'])) { @@ -133,7 +143,13 @@ switch ($_GET['v'] ?? null) { '); $removeTag->bindValue('change_id', $changeId); $removeTag->bindValue('tag_id', $_POST['remove_tag']); - $removeTag->execute(); + + if ($removeTag->execute()) { + audit_log('CHANGELOG_TAG_REMOVE', $app->getUserId(), [ + $changeId, + $_POST['remove_tag'] + ]); + } } } @@ -267,8 +283,12 @@ switch ($_GET['v'] ?? null) { $updateTag->execute(); if ($tagId < 1) { - header('Location: ?v=tag&t=' . Database::lastInsertId()); + $tagId = Database::lastInsertId(); + audit_log('CHANGELOG_TAG_EDIT', $app->getUserId(), [$tagId]); + header('Location: ?v=tag&t=' . $tagId); return; + } else { + audit_log('CHANGELOG_TAG_CREATE', $app->getUserId(), [$tagId]); } } } @@ -374,8 +394,12 @@ switch ($_GET['v'] ?? null) { $updateAction->execute(); if ($actionId < 1) { - header('Location: ?v=action&a=' . Database::lastInsertId()); + $actionId = Database::lastInsertId(); + audit_log('CHANGELOG_ACTION_CREATE', $app->getUserId(), [$actionId]); + header('Location: ?v=action&a=' . $actionId); return; + } else { + audit_log('CHANGELOG_ACTION_EDIT', $app->getUserId(), [$actionId]); } } } diff --git a/public/manage/index.php b/public/manage/index.php index eb04a280..61d71f46 100644 --- a/public/manage/index.php +++ b/public/manage/index.php @@ -1,16 +1,22 @@ getTemplating(); +$generalPerms = perms_get_user(MSZ_PERMS_GENERAL, $app->getUserId()); +$tpl = $app->getTemplating(); switch ($_GET['v'] ?? null) { default: case 'overview': - echo $templating->render('@manage.general.overview'); + echo $tpl->render('@manage.general.overview'); break; case 'logs': - echo 'soon'; + if (!perms_check($generalPerms, MSZ_GENERAL_PERM_VIEW_LOGS)) { + echo render_error(403); + break; + } + + var_dump(audit_log_list(0, 20)); break; case 'emoticons': diff --git a/public/settings.php b/public/settings.php index 8529dde3..7138d6fa 100644 --- a/public/settings.php +++ b/public/settings.php @@ -196,6 +196,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } $updateAccountFields['email'] = strtolower($_POST['email']['new']); + + audit_log('PERSONAL_EMAIL_CHANGE', $app->getUserId(), [ + $updateAccountFields['email'], + ]); } if (!empty($_POST['password']['new'])) { @@ -213,6 +217,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } $updateAccountFields['password'] = user_password_hash($_POST['password']['new']); + + audit_log('PERSONAL_PASSWORD_CHANGE', $app->getUserId()); } if (count($updateAccountFields) > 0) { @@ -311,6 +317,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } user_session_delete($session['session_id']); + audit_log('PERSONAL_SESSION_DESTROY', $app->getUserId(), [ + $session['session_id'], + ]); break; } } diff --git a/src/audit_log.php b/src/audit_log.php new file mode 100644 index 00000000..3351b100 --- /dev/null +++ b/src/audit_log.php @@ -0,0 +1,61 @@ +bindValue('action', $action); + $addLog->bindValue('user', $userId < 1 ? null : $userId); + $addLog->bindValue('params', json_encode($params)); + $addLog->bindValue('log_ip', $ipAddress->getRaw()); + $addLog->execute(); +} + +function audit_log_list(int $offset, int $take, int $userId = 0): array +{ + $offset = max(0, $offset); + $take = max(1, $take); + + $getLogs = Database::prepare(sprintf(' + SELECT + l.`log_id`, l.`log_action`, l.`log_params`, l.`log_created`, + u.`user_id`, u.`username`, + INET6_NTOA(l.`log_ip`) as `log_ip`, + COALESCE(r.`role_colour`, CAST(0x40000000 AS UNSIGNED)) as `user_colour` + FROM `msz_audit_log` as l + LEFT JOIN `msz_users` as u + ON u.`user_id` = l.`user_id` + LEFT JOIN `msz_roles` as r + ON r.`role_id` = u.`display_role` + WHERE %s + ORDER BY l.`log_id` DESC + LIMIT :offset, :take + ', $userId < 1 ? '1' : 'l.`user_id` = :user_id')); + + if ($userId >= 1) { + $getLogs->bindValue('user_id'); + } + + $getLogs->bindValue('offset', $offset); + $getLogs->bindValue('take', $take); + + return $getLogs->execute() ? $getLogs->fetchAll(PDO::FETCH_ASSOC) : []; +} diff --git a/src/manage.php b/src/manage.php index 5aea2f8c..43193790 100644 --- a/src/manage.php +++ b/src/manage.php @@ -28,11 +28,11 @@ function manage_get_menu(int $userId): array $menu['General'][] = '_'; if (perms_check($perms['general'], MSZ_GENERAL_PERM_MANAGE_EMOTICONS)) { - $menu['General']['Emoticons'] = '/manage/users.php?v=emoticons'; + $menu['General']['Emoticons'] = '/manage/index.php?v=emoticons'; } if (perms_check($perms['general'], MSZ_GENERAL_PERM_MANAGE_SETTINGS)) { - $menu['General']['Settings'] = '/manage/users.php?v=settings'; + $menu['General']['Settings'] = '/manage/index.php?v=settings'; } } diff --git a/views/manage/master.twig b/views/manage/master.twig index f3f5f644..8450e2e3 100644 --- a/views/manage/master.twig +++ b/views/manage/master.twig @@ -2,7 +2,7 @@ - Flashii Broom Closet + {{ globals.site_name|default('The') }} Broom Closet