diff options
| -rw-r--r-- | Digitigrade/GlobalSettings.php | 13 | ||||
| -rw-r--r-- | Digitigrade/Model/UserAccount.php | 13 | ||||
| -rwxr-xr-x | bin/migrate | 3 | ||||
| -rw-r--r-- | locale/en_GB.json | 5 | ||||
| -rw-r--r-- | migrations/20250118_181312_add_user_account_admin.php | 8 | ||||
| -rw-r--r-- | misc/render_template.php | 5 | ||||
| -rw-r--r-- | routes/admin.php | 27 | ||||
| -rw-r--r-- | settings.global.ini | 4 | ||||
| -rw-r--r-- | static/form.css | 27 | ||||
| -rw-r--r-- | static/misc.css | 14 | ||||
| -rw-r--r-- | templates/global_settings_page.php | 6 | ||||
| -rw-r--r-- | templates/settings_editor.php | 27 | ||||
| -rw-r--r-- | templates/settings_field.php | 31 | ||||
| -rw-r--r-- | templates/skeleton.php | 23 |
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> |
