aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorwinter2025-01-18 20:02:39 +0000
committerwinter2025-01-18 20:02:39 +0000
commit66bdcf04f431d615b5fac93e7677e7304888e90c (patch)
treed75f1b299e8ed2ff8d2fd6ccd42185cfb06b6729
parent00ab9734e880219fa68fed98cefda5ce12958e4b (diff)
implement settings editor page
-rw-r--r--Digitigrade/GlobalSettings.php13
-rw-r--r--Digitigrade/Model/UserAccount.php13
-rwxr-xr-xbin/migrate3
-rw-r--r--locale/en_GB.json5
-rw-r--r--migrations/20250118_181312_add_user_account_admin.php8
-rw-r--r--misc/render_template.php5
-rw-r--r--routes/admin.php27
-rw-r--r--settings.global.ini4
-rw-r--r--static/form.css27
-rw-r--r--static/misc.css14
-rw-r--r--templates/global_settings_page.php6
-rw-r--r--templates/settings_editor.php27
-rw-r--r--templates/settings_field.php31
-rw-r--r--templates/skeleton.php23
14 files changed, 189 insertions, 17 deletions
diff --git a/Digitigrade/GlobalSettings.php b/Digitigrade/GlobalSettings.php
index 435c85a..52c6582 100644
--- a/Digitigrade/GlobalSettings.php
+++ b/Digitigrade/GlobalSettings.php
@@ -26,6 +26,19 @@ class GlobalSettings extends Singleton {
}
/**
+ * Gets a boolean setting's value.
+ * @param string $key which setting to get
+ * @return ?bool the value, or null if not set
+ */
+ public function getBool(string $key): ?bool {
+ return match ($this->get($key)) {
+ '', 'false' => false,
+ '1', 'true' => true,
+ default => null
+ };
+ }
+
+ /**
* Sets the value of a setting.
* @param string $key which setting to modify
* @param string $value the new value
diff --git a/Digitigrade/Model/UserAccount.php b/Digitigrade/Model/UserAccount.php
index d0fa94b..37e0c7e 100644
--- a/Digitigrade/Model/UserAccount.php
+++ b/Digitigrade/Model/UserAccount.php
@@ -1,6 +1,7 @@
<?php
namespace Digitigrade\Model;
+use Digitigrade\HttpResponseStatus\Forbidden;
use Digitigrade\HttpResponseStatus\Unauthorized;
use Digitigrade\Model;
@@ -10,6 +11,7 @@ class UserAccount extends Model {
public string $passwordHash;
public ?Actor $actor;
public bool $banned = false;
+ public bool $isAdmin = false;
public static function create(string $email, #[\SensitiveParameter] string $password): self {
$user = new self();
@@ -66,4 +68,15 @@ class UserAccount extends Model {
public function verifyPassword(#[\SensitiveParameter] $attemptedPassword): bool {
return password_verify($attemptedPassword, $this->passwordHash);
}
+
+ /**
+ * Throws if this user isn't an admin.
+ * @return self this user (so you can use it in chains)
+ */
+ public function requireAdmin(): self {
+ if (!$this->isAdmin) {
+ throw new Forbidden();
+ }
+ return $this;
+ }
} \ No newline at end of file
diff --git a/bin/migrate b/bin/migrate
index 6c1c71d..fad88ff 100755
--- a/bin/migrate
+++ b/bin/migrate
@@ -7,4 +7,5 @@ foreach (glob(__DIR__ . '/../migrations/*.php') as $file) {
require $file;
}
-\Digitigrade\Db\Migrator::getInstance()->migrate(); \ No newline at end of file
+\Digitigrade\Db\Migrator::getInstance()->migrate();
+\Digitigrade\GlobalSettings::getInstance()->mergeUpdates(); \ No newline at end of file
diff --git a/locale/en_GB.json b/locale/en_GB.json
index 5706543..2eac456 100644
--- a/locale/en_GB.json
+++ b/locale/en_GB.json
@@ -38,6 +38,7 @@
"navigation.login": "Log in",
"navigation.logout": "Log out",
"navigation.followRequests": "Follow requests",
+ "navigation.instanceSettings": "Instance settings",
"login.pageTitle": "Log in",
"login.email": "Email address",
"login.password": "Password",
@@ -55,7 +56,9 @@
"followRequests.accepted": "Accepted!",
"followRequests.reject.action": "Reject",
"followRequests.rejected": "Rejected!",
+ "globalSettings.pageTitle": "Global settings in category '%s'",
"form.edit": "Edit",
"form.saveChanges": "Save",
- "form.discardChanges": "Cancel"
+ "form.discardChanges": "Cancel",
+ "form.saved": "Saved!"
}
diff --git a/migrations/20250118_181312_add_user_account_admin.php b/migrations/20250118_181312_add_user_account_admin.php
new file mode 100644
index 0000000..274c917
--- /dev/null
+++ b/migrations/20250118_181312_add_user_account_admin.php
@@ -0,0 +1,8 @@
+<?php
+
+use \Digitigrade\Db\Migrator;
+
+Migrator::getInstance()->register(20250118_181312, function (PDO $db) {
+ // admin status for users
+ $db->exec('ALTER TABLE user_account ADD COLUMN is_admin boolean not null default false');
+});
diff --git a/misc/render_template.php b/misc/render_template.php
index 294fc16..1b64f0c 100644
--- a/misc/render_template.php
+++ b/misc/render_template.php
@@ -12,9 +12,10 @@ function render_template(string $templateName, array $args = []) {
global $__spooky_magical_template_global_state;
$__spooky_magical_template_global_state['slotParentArgs'] = $__spooky_magical_template_global_state['slotArgs'] ?? null;
$__spooky_magical_template_global_state['slotArgs'] = $args;
- foreach ($args as $key => $value) {
- ${$key} = $value;
+ foreach ($args as $__template_key => $__template_value) {
+ ${$__template_key} = $__template_value;
}
+ unset($__template_key, $__template_value);
if (!function_exists('slot')) {
function slot() {
diff --git a/routes/admin.php b/routes/admin.php
new file mode 100644
index 0000000..9915fde
--- /dev/null
+++ b/routes/admin.php
@@ -0,0 +1,27 @@
+<?php
+
+use Digitigrade\GlobalSettings;
+use Digitigrade\Model\UserAccount;
+use Digitigrade\Router;
+
+Router::getInstance()->mount('/admin/settings/:prefix', function (array $args) {
+ UserAccount::requireByCurrentSession()->requireAdmin();
+ render_template('global_settings_page', ['prefix' => $args['prefix']]);
+});
+
+Router::getInstance()->mount('/fragment/admin/settings/:prefix', function (array $args) {
+ UserAccount::requireByCurrentSession()->requireAdmin();
+ $saved = false;
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ // update all the settings from the page
+ $settings = GlobalSettings::getInstance();
+ foreach ($_POST as $key => $value) {
+ // same workaround as in settings_field
+ $key = str_replace('<DOT>', '.', $key);
+ error_log("setting '$key' to '$value'");
+ $settings->set($key, $value);
+ }
+ $saved = true;
+ }
+ render_template('settings_editor', ['prefix' => $args['prefix'], 'saved' => $saved]);
+}); \ No newline at end of file
diff --git a/settings.global.ini b/settings.global.ini
index 78c2142..79699d1 100644
--- a/settings.global.ini
+++ b/settings.global.ini
@@ -11,5 +11,5 @@ default = "A Digitigrade instance"
[instance.openRegistrations]
name = "Open registrations"
description = "Allow new users to sign up?"
-default = ""
-type = boolean \ No newline at end of file
+default = false
+type = boolean
diff --git a/static/form.css b/static/form.css
index a109c7f..79509d7 100644
--- a/static/form.css
+++ b/static/form.css
@@ -26,7 +26,7 @@ form {
font-size: inherit;
color: inherit;
background: var(--clr-field-background);
- width: min(60vw, 24em, 100%);
+ width: min(24em, 100%);
box-sizing: border-box;
}
textarea {
@@ -57,4 +57,29 @@ form {
box-sizing: border-box;
}
}
+
+ &.settings {
+ padding: 0;
+ gap: 0;
+ .row {
+ display: grid;
+ grid-template-columns: 1fr 16em;
+ grid-auto-flow: column;
+ gap: var(--spacing-double);
+ align-items: center;
+ padding: var(--spacing-single) var(--spacing-double);
+ &:not(:last-child) {
+ border-bottom: var(--border);
+ }
+ @media (width < 600px) {
+ grid-template-columns: 1fr;
+ grid-auto-flow: row;
+ gap: var(--spacing-single);
+ }
+ }
+ label {
+ color: var(--clr-foreground);
+ font-size: var(--font-normal-size);
+ }
+ }
}
diff --git a/static/misc.css b/static/misc.css
index ff9fe99..9bf9490 100644
--- a/static/misc.css
+++ b/static/misc.css
@@ -93,6 +93,20 @@ pre {
text-wrap: wrap;
}
+.temporaryIndicator {
+ color: var(--clr-positive);
+ animation: fadeOut 1s forwards;
+ animation-delay: 2s;
+}
+@keyframes fadeOut {
+ 0% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
[hidden] {
display: none;
}
diff --git a/templates/global_settings_page.php b/templates/global_settings_page.php
new file mode 100644
index 0000000..fcaf7ab
--- /dev/null
+++ b/templates/global_settings_page.php
@@ -0,0 +1,6 @@
+<?php call_template('skeleton', [
+ 'pageTitle' => sprintf(__('globalSettings.pageTitle'), $prefix)
+], function () {
+ global $prefix;
+ call_template('settings_editor', ['prefix' => $prefix]);
+}); \ No newline at end of file
diff --git a/templates/settings_editor.php b/templates/settings_editor.php
new file mode 100644
index 0000000..1bf72b7
--- /dev/null
+++ b/templates/settings_editor.php
@@ -0,0 +1,27 @@
+<?php
+use Digitigrade\GlobalSettings;
+
+$settings = GlobalSettings::getInstance();
+?>
+<form class="settings nopanel" action="/todo" hx-post="/fragment/admin/settings/<?= $prefix ?>" hx-swap="outerHTML"
+ hx-disabled-elt="find button">
+ <?php foreach ($settings->getAllInPrefix($prefix) as $key => $value) {
+ $info = $settings->getInfo($key);
+ call_template('settings_field', [
+ 'key' => $key,
+ 'value' => $value,
+ 'name' => $info['name'] ?? $key,
+ 'description' => $info['description'] ?? '',
+ 'options' => $info['options'] ?? null,
+ 'type' => $info['type'] ?? 'string'
+ ]);
+ } ?>
+ <div class="row">
+ <div>
+ <?php if ($saved ?? false): ?>
+ <span class="temporaryIndicator"><?= __('form.saved') ?></span>
+ <?php endif; ?>
+ </div>
+ <button class="primary"><?= __('form.saveChanges') ?></button>
+ </div>
+</form> \ No newline at end of file
diff --git a/templates/settings_field.php b/templates/settings_field.php
new file mode 100644
index 0000000..15b6c71
--- /dev/null
+++ b/templates/settings_field.php
@@ -0,0 +1,31 @@
+<?php
+$id = "setting-$key";
+$inputType = 'text';
+if (($type ?? null) == 'boolean') {
+ $inputType = 'select';
+ $options = ['true', 'false'];
+}
+// php converts . to _ in $_POST for some mikuforsaken reason so i'm working around it like this
+$fieldName = str_replace('.', '<DOT>', $key);
+?>
+<div class="row">
+
+ <label for="<?= $id ?>">
+ <strong><?= $name ?></strong><br>
+ <?= $description ?>
+ </label>
+
+ <?php if ($inputType == 'select'): ?>
+
+ <select id="<?= $id ?>" name="<?= $fieldName ?>">
+ <?php foreach ($options as $o): ?>
+ <option <?= $value == $o ? 'selected' : '' ?>><?= $o ?></option>
+ <?php endforeach; ?>
+ </select>
+
+ <?php else: ?>
+
+ <input id="<?= $id ?>" name="<?= $fieldName ?>" value="<?= $value ?>" type="<?= $inputType ?>" autocomplete="off">
+
+ <?php endif; ?>
+</div> \ No newline at end of file
diff --git a/templates/skeleton.php b/templates/skeleton.php
index b274484..3aaf132 100644
--- a/templates/skeleton.php
+++ b/templates/skeleton.php
@@ -1,5 +1,6 @@
<?php
+use Digitigrade\GlobalSettings;
use Digitigrade\Model\FollowRelation;
$user = Digitigrade\Model\UserAccount::findByCurrentSession();
?>
@@ -28,7 +29,7 @@ $user = Digitigrade\Model\UserAccount::findByCurrentSession();
]);
call_template('navigation_links', ['links' => $links]);
?>
- <span id="siteTitle"><?= __('digitigrade') ?></span>
+ <span id="siteTitle"><?= GlobalSettings::getInstance()->get('instance.name') ?></span>
<img id="loadingIndicator" class="htmx-indicator" src="/static/tail-spin.svg">
<?php if ($user == null):
call_template('navigation_links', ['links' => [
@@ -47,15 +48,17 @@ $user = Digitigrade\Model\UserAccount::findByCurrentSession();
<?php
if ($user != null) {
call_template('write_note_form');
- call_template('side_navigation', ['links' => [
- [
- 'href' => '/followrequests',
- 'icon' => 'person_add',
- 'label' => __('navigation.followRequests'),
- 'unreadCount' => FollowRelation::countWhere("object = ? AND status = 'pending'", [$user->actor->id])
- ],
- ['href' => '/logout', 'icon' => 'logout', 'label' => __('navigation.logout')]
- ]]);
+ $links = [[
+ 'href' => '/followrequests',
+ 'icon' => 'person_add',
+ 'label' => __('navigation.followRequests'),
+ 'unreadCount' => FollowRelation::countWhere("object = ? AND status = 'pending'", [$user->actor->id])
+ ]];
+ if ($user->isAdmin) {
+ $links[] = ['href' => '/admin/settings/instance', 'icon' => 'settings', 'label' => __('navigation.instanceSettings')];
+ }
+ $links[] = ['href' => '/logout', 'icon' => 'logout', 'label' => __('navigation.logout')];
+ call_template('side_navigation', ['links' => $links]);
}
?>
</section>