@geekity/cms 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/admin/components/admin-bar.njk +27 -6
- package/admin/components/alert.njk +28 -0
- package/admin/components/badge.njk +58 -0
- package/admin/components/button.njk +56 -0
- package/admin/components/card.njk +40 -0
- package/admin/components/collapse.njk +36 -0
- package/admin/components/copy.njk +20 -0
- package/admin/components/dropdown.njk +32 -0
- package/admin/components/fields.njk +81 -45
- package/admin/components/flash.njk +12 -5
- package/admin/components/menu.njk +51 -0
- package/admin/components/navbar.njk +23 -0
- package/admin/components/pagination.njk +47 -0
- package/admin/components/public-admin-bar.njk +4 -41
- package/admin/components/stat.njk +35 -0
- package/admin/components/table.njk +74 -0
- package/admin/components/tabs.njk +33 -0
- package/admin/components/theme-choice.njk +18 -0
- package/admin/layouts/base.njk +9 -16
- package/admin/layouts/settings-page.njk +15 -10
- package/admin/layouts/shell.njk +23 -31
- package/admin/pages/account/forgot.njk +39 -33
- package/admin/pages/account/login.njk +22 -16
- package/admin/pages/account/reset.njk +34 -25
- package/admin/pages/account/setup.njk +21 -15
- package/admin/pages/appearance/themes.njk +50 -44
- package/admin/pages/comments/all.njk +88 -106
- package/admin/pages/dashboard/home.njk +50 -62
- package/admin/pages/documents/conflict.njk +29 -24
- package/admin/pages/documents/editor.njk +155 -275
- package/admin/pages/documents/list.njk +60 -69
- package/admin/pages/documents/syndication.njk +98 -60
- package/admin/pages/documents/taxonomy.njk +36 -32
- package/admin/pages/error.njk +8 -5
- package/admin/pages/federation/followers.njk +91 -97
- package/admin/pages/federation/settings.njk +10 -11
- package/admin/pages/indieauth/consent.njk +60 -44
- package/admin/pages/indieauth/refused.njk +6 -3
- package/admin/pages/media/library.njk +86 -107
- package/admin/pages/messages/all.njk +71 -77
- package/admin/pages/navigation/menus.njk +90 -85
- package/admin/pages/placeholder.njk +13 -8
- package/admin/pages/settings/discussion.njk +30 -29
- package/admin/pages/settings/email.njk +26 -28
- package/admin/pages/settings/general.njk +10 -4
- package/admin/pages/settings/permalinks.njk +8 -9
- package/admin/pages/settings/privacy.njk +3 -3
- package/admin/pages/settings/reading.njk +3 -3
- package/admin/pages/tools/content-index.njk +79 -73
- package/admin/pages/tools/personal-data.njk +61 -55
- package/admin/pages/users/activity-entry.njk +12 -12
- package/admin/pages/users/activity.njk +13 -19
- package/admin/pages/users/apps.njk +112 -100
- package/admin/pages/users/edit.njk +65 -58
- package/admin/pages/users/list.njk +26 -24
- package/admin/pages/users/new.njk +11 -7
- package/admin/src/admin.css +174 -0
- package/admin/static/admin-bar.css +33 -31
- package/admin/static/admin-bar.js +22 -0
- package/admin/static/admin.css +7388 -1195
- package/admin/static/copy.js +1 -1
- package/admin/static/editor.js +18 -18
- package/dist/admin/accounts.d.ts +20 -0
- package/dist/admin/accounts.d.ts.map +1 -1
- package/dist/admin/accounts.js +25 -0
- package/dist/admin/accounts.js.map +1 -1
- package/dist/admin/admin-theme.d.ts +157 -0
- package/dist/admin/admin-theme.d.ts.map +1 -0
- package/dist/admin/admin-theme.js +52 -0
- package/dist/admin/admin-theme.js.map +1 -0
- package/dist/admin/assets.d.ts +4 -3
- package/dist/admin/assets.d.ts.map +1 -1
- package/dist/admin/assets.js +4 -3
- package/dist/admin/assets.js.map +1 -1
- package/dist/admin/documents.js +1 -1
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/headers.js +1 -1
- package/dist/admin/headers.js.map +1 -1
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +17 -8
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/templates.d.ts +6 -3
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +25 -3
- package/dist/admin/templates.js.map +1 -1
- package/dist/admin/users.d.ts +4 -0
- package/dist/admin/users.d.ts.map +1 -1
- package/dist/admin/users.js +37 -1
- package/dist/admin/users.js.map +1 -1
- package/dist/content/post-type.d.ts.map +1 -1
- package/dist/content/post-type.js +7 -2
- package/dist/content/post-type.js.map +1 -1
- package/dist/web/admin-bar.d.ts +4 -4
- package/dist/web/admin-bar.d.ts.map +1 -1
- package/dist/web/admin-bar.js +11 -34
- package/dist/web/admin-bar.js.map +1 -1
- package/package.json +8 -4
- package/themes/default/README.md +3 -1
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
so a screen reader moving between buttons hears which one it is on. #}
|
|
5
5
|
{% extends "layouts/shell.njk" %}
|
|
6
6
|
{% import "components/fields.njk" as field %}
|
|
7
|
+
{% from "components/alert.njk" import alert %}
|
|
8
|
+
{% from "components/badge.njk" import badge %}
|
|
9
|
+
{% from "components/button.njk" import button %}
|
|
10
|
+
{% from "components/table.njk" import table %}
|
|
7
11
|
|
|
8
12
|
{% block title %}Connected apps{% endblock %}
|
|
9
13
|
|
|
10
14
|
{% block content %}
|
|
11
|
-
<h1>Connected apps</h1>
|
|
15
|
+
<h1 class="mb-6 text-2xl font-bold">Connected apps</h1>
|
|
12
16
|
|
|
13
17
|
{% if connections | length %}
|
|
14
|
-
<p class="
|
|
18
|
+
<p class="mb-4 max-w-prose text-sm">
|
|
15
19
|
These apps signed in as you and can act as you, within the permissions
|
|
16
20
|
listed. An app that keeps using its connection stays connected; one left
|
|
17
21
|
unused is disconnected on the date shown. A token you created works
|
|
@@ -19,8 +23,7 @@
|
|
|
19
23
|
refused, and it has to ask you again.
|
|
20
24
|
</p>
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
<caption class="admin-visually-hidden">Connected apps</caption>
|
|
26
|
+
{% call table('Connected apps') %}
|
|
24
27
|
<thead>
|
|
25
28
|
<tr>
|
|
26
29
|
<th scope="col">App</th>
|
|
@@ -36,9 +39,9 @@
|
|
|
36
39
|
<tr>
|
|
37
40
|
<td>
|
|
38
41
|
{% if connection.url %}
|
|
39
|
-
<a href="{{ connection.url }}">{{ connection.name }}</a>
|
|
42
|
+
<a class="link link-hover font-medium" href="{{ connection.url }}">{{ connection.name }}</a>
|
|
40
43
|
{% else %}
|
|
41
|
-
{{ connection.name }}
|
|
44
|
+
<span class="font-medium">{{ connection.name }}</span> {{ badge('Token you created') }}
|
|
42
45
|
{% endif %}
|
|
43
46
|
</td>
|
|
44
47
|
<td>
|
|
@@ -50,29 +53,29 @@
|
|
|
50
53
|
{% endfor %}
|
|
51
54
|
</ul>
|
|
52
55
|
</td>
|
|
53
|
-
<td><time datetime="{{ connection.issued.iso }}">{{ connection.issued.text }}</time></td>
|
|
54
|
-
<td>
|
|
56
|
+
<td class="whitespace-nowrap"><time datetime="{{ connection.issued.iso }}">{{ connection.issued.text }}</time></td>
|
|
57
|
+
<td class="whitespace-nowrap">
|
|
55
58
|
{% if connection.lastUsed %}
|
|
56
59
|
<time datetime="{{ connection.lastUsed.iso }}">{{ connection.lastUsed.text }}</time>
|
|
57
60
|
{% else %}
|
|
58
|
-
|
|
61
|
+
Not yet
|
|
59
62
|
{% endif %}
|
|
60
63
|
</td>
|
|
61
|
-
<td><time datetime="{{ connection.expires.iso }}">{{ connection.expires.text }}</time></td>
|
|
62
|
-
<td
|
|
64
|
+
<td class="whitespace-nowrap"><time datetime="{{ connection.expires.iso }}">{{ connection.expires.text }}</time></td>
|
|
65
|
+
<td>
|
|
63
66
|
<form method="post" action="{{ revokeUrl }}">
|
|
64
67
|
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
65
68
|
<input type="hidden" name="{{ connectionField }}" value="{{ connection.id }}" />
|
|
66
|
-
|
|
69
|
+
{{ button('Revoke ' + connection.name, type='submit', style='ghost', size='xs') }}
|
|
67
70
|
</form>
|
|
68
71
|
</td>
|
|
69
72
|
</tr>
|
|
70
73
|
{% endfor %}
|
|
71
74
|
</tbody>
|
|
72
|
-
|
|
75
|
+
{% endcall %}
|
|
73
76
|
{% else %}
|
|
74
77
|
<p>No apps are connected to your account.</p>
|
|
75
|
-
<p class="
|
|
78
|
+
<p class="mt-2 max-w-prose text-sm">
|
|
76
79
|
An app connects here when you sign in to it with your site's address and
|
|
77
80
|
approve what it asks for: a Micropub app such as Quill that posts to
|
|
78
81
|
your site, or an MCP client such as an AI assistant. Each one you approve
|
|
@@ -80,101 +83,110 @@
|
|
|
80
83
|
</p>
|
|
81
84
|
{% endif %}
|
|
82
85
|
|
|
83
|
-
<
|
|
86
|
+
<section class="mt-10 max-w-2xl">
|
|
87
|
+
<h2 id="create-token" class="mb-3 text-lg font-semibold">Create a token</h2>
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
{% if createToken.created %}
|
|
90
|
+
<div class="mb-4">
|
|
91
|
+
{% call alert(color='success', style='soft', polite=true) %}
|
|
92
|
+
<div class="w-full">
|
|
93
|
+
{{ field.text('created-token', 'created-token', 'Token for ' ~ createToken.created.name, createToken.created.token,
|
|
94
|
+
readonly=true, spellcheck='false',
|
|
95
|
+
hint='Copy it now. It will not be shown again: the site keeps only a fingerprint of it.') }}
|
|
96
|
+
</div>
|
|
97
|
+
{% endcall %}
|
|
98
|
+
</div>
|
|
99
|
+
{% endif %}
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
{% if createToken.problems.name or createToken.problems.scopes or createToken.problems.expires %}
|
|
102
|
+
{% call field.summary('Nothing was saved. Fix the fields marked below and try again.') %}
|
|
103
|
+
{{ field.problem('create-token-name', createToken.problems.name) }}
|
|
104
|
+
{{ field.problem('create-token-scope-profile', createToken.problems.scopes) }}
|
|
105
|
+
{{ field.problem('create-token-expires', createToken.problems.expires) }}
|
|
106
|
+
{% endcall %}
|
|
107
|
+
{% endif %}
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
<p class="mb-2 max-w-prose text-sm">
|
|
110
|
+
Some tools and scripts cannot sign in, but accept a token you paste in,
|
|
111
|
+
such as the Manual tab of micropub.rocks. A token acts as you on this
|
|
112
|
+
site's Micropub endpoint, with only the permissions you tick, until it
|
|
113
|
+
expires or you revoke it above.
|
|
114
|
+
</p>
|
|
107
115
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
116
|
+
<form method="post" action="{{ createToken.url }}">
|
|
117
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
118
|
+
{{ field.text('create-token-name', 'name', 'Name', createToken.name,
|
|
119
|
+
error=createToken.problems.name, required=true,
|
|
120
|
+
hint='What will use it, such as Publishing script.') }}
|
|
121
|
+
<fieldset class="fieldset">
|
|
122
|
+
<legend class="fieldset-legend">It may</legend>
|
|
123
|
+
{% for scope in createToken.scopes %}
|
|
124
|
+
{{ field.checkbox('create-token-scope-' ~ scope.name, 'scope', scope.label, checked=scope.checked, value=scope.name,
|
|
125
|
+
error=(createToken.problems.scopes if loop.first else '')) }}
|
|
126
|
+
{% endfor %}
|
|
127
|
+
</fieldset>
|
|
128
|
+
{% call field.select('create-token-expires', 'expires', 'Expires after', error=createToken.problems.expires) %}
|
|
129
|
+
{% for expiry in createToken.expiries %}
|
|
130
|
+
<option value="{{ expiry.days }}"{% if expiry.selected %} selected{% endif %}>{% if expiry.days == 365 %}1 year{% else %}{{ expiry.days }} days{% endif %}</option>
|
|
131
|
+
{% endfor %}
|
|
132
|
+
{% endcall %}
|
|
133
|
+
<div class="mt-4">{{ button('Create token', type='submit', color='primary') }}</div>
|
|
134
|
+
</form>
|
|
135
|
+
</section>
|
|
127
136
|
|
|
128
|
-
<
|
|
137
|
+
<section class="mt-10">
|
|
138
|
+
<h2 id="without-pkce" class="mb-3 text-lg font-semibold">Apps allowed without PKCE</h2>
|
|
129
139
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
{% if withoutPkce.problem %}
|
|
141
|
+
<div class="max-w-2xl">
|
|
142
|
+
{% call field.summary('Nothing was saved. Fix the field marked below and try again.') %}
|
|
143
|
+
{{ field.problem('without-pkce-client', withoutPkce.problem) }}
|
|
144
|
+
{% endcall %}
|
|
145
|
+
</div>
|
|
146
|
+
{% endif %}
|
|
135
147
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
<p class="mb-4 max-w-prose text-sm">
|
|
149
|
+
PKCE protects a sign-in when somebody intercepts the one-time code an app
|
|
150
|
+
is handed: without the secret the app kept, the code is useless. Every app
|
|
151
|
+
must use it, unless you list it here. List only an app you trust that
|
|
152
|
+
cannot sign in otherwise, such as iA Writer. A listed app is let in without
|
|
153
|
+
PKCE only when it sends you back to an https address on its own host, and
|
|
154
|
+
the consent screen tells you when it is.
|
|
155
|
+
</p>
|
|
144
156
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<thead>
|
|
149
|
-
<tr>
|
|
150
|
-
<th scope="col">client_id</th>
|
|
151
|
-
<th scope="col">Actions</th>
|
|
152
|
-
</tr>
|
|
153
|
-
</thead>
|
|
154
|
-
<tbody>
|
|
155
|
-
{% for client in withoutPkce.clients %}
|
|
157
|
+
{% if withoutPkce.clients | length %}
|
|
158
|
+
{% call table('Apps allowed without PKCE') %}
|
|
159
|
+
<thead>
|
|
156
160
|
<tr>
|
|
157
|
-
<
|
|
158
|
-
<
|
|
159
|
-
<form method="post" action="{{ withoutPkce.removeUrl }}">
|
|
160
|
-
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
161
|
-
<input type="hidden" name="{{ withoutPkce.field }}" value="{{ client }}" />
|
|
162
|
-
<button type="submit">Remove {{ client }}</button>
|
|
163
|
-
</form>
|
|
164
|
-
</td>
|
|
161
|
+
<th scope="col">client_id</th>
|
|
162
|
+
<th scope="col">Actions</th>
|
|
165
163
|
</tr>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
164
|
+
</thead>
|
|
165
|
+
<tbody>
|
|
166
|
+
{% for client in withoutPkce.clients %}
|
|
167
|
+
<tr>
|
|
168
|
+
<td><code>{{ client }}</code></td>
|
|
169
|
+
<td>
|
|
170
|
+
<form method="post" action="{{ withoutPkce.removeUrl }}">
|
|
171
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
172
|
+
<input type="hidden" name="{{ withoutPkce.field }}" value="{{ client }}" />
|
|
173
|
+
{{ button('Remove ' + client, type='submit', style='ghost', size='xs') }}
|
|
174
|
+
</form>
|
|
175
|
+
</td>
|
|
176
|
+
</tr>
|
|
177
|
+
{% endfor %}
|
|
178
|
+
</tbody>
|
|
179
|
+
{% endcall %}
|
|
180
|
+
{% else %}
|
|
181
|
+
<p>No apps are listed. Every app must use PKCE to sign in.</p>
|
|
182
|
+
{% endif %}
|
|
172
183
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
<form class="mt-4 max-w-2xl" method="post" action="{{ withoutPkce.addUrl }}">
|
|
185
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
186
|
+
{{ field.text('without-pkce-client', withoutPkce.field, 'App’s client_id', withoutPkce.value,
|
|
187
|
+
error=withoutPkce.problem, inputmode='url', spellcheck='false', required=true,
|
|
188
|
+
hint='The URL the app names itself by when it signs in, as App activity shows it, such as <code>https://ia.net/writer</code>.') }}
|
|
189
|
+
<div class="mt-4">{{ button('Allow without PKCE', type='submit', color='primary') }}</div>
|
|
190
|
+
</form>
|
|
191
|
+
</section>
|
|
180
192
|
{% endblock %}
|
|
@@ -12,25 +12,29 @@
|
|
|
12
12
|
lands back here, so a Save always shows its own result. #}
|
|
13
13
|
{% extends "layouts/shell.njk" %}
|
|
14
14
|
{% import "components/fields.njk" as field %}
|
|
15
|
+
{% from "components/badge.njk" import badge, status %}
|
|
16
|
+
{% from "components/button.njk" import button %}
|
|
17
|
+
{% from "components/card.njk" import card, cardActions %}
|
|
15
18
|
|
|
16
19
|
{% block title %}Edit {{ account.username }}{% endblock %}
|
|
17
20
|
|
|
18
21
|
{% block content %}
|
|
19
|
-
<h1>Edit {{ account.username }}{% if account.you %}
|
|
22
|
+
<h1 class="mb-2 flex flex-wrap items-center gap-2 text-2xl font-bold">Edit {{ account.username }}{% if account.you %} {{ badge('(you)', style='outline', size='md') }}{% endif %}</h1>
|
|
20
23
|
|
|
21
|
-
<p class="
|
|
24
|
+
<p class="mb-6 text-sm"><a class="link" href="{{ usersUrl }}">Back to all users</a></p>
|
|
22
25
|
|
|
23
|
-
<
|
|
26
|
+
<div class="flex max-w-2xl flex-col gap-6">
|
|
27
|
+
|
|
28
|
+
{% call card(title='Account') %}
|
|
29
|
+
<form method="post" action="{{ userEmailUrl }}">
|
|
24
30
|
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
25
31
|
<input type="hidden" name="{{ fields.userId }}" value="{{ account.id }}" />
|
|
26
32
|
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
<p class="admin-field-static">
|
|
33
|
+
<p>
|
|
30
34
|
Signs in as <strong>{{ account.username }}</strong>, and writes at
|
|
31
|
-
<a href="{{ account.archiveUrl }}">{{ account.archiveUrl }}</a>.
|
|
35
|
+
<a class="link break-all" href="{{ account.archiveUrl }}">{{ account.archiveUrl }}</a>.
|
|
32
36
|
</p>
|
|
33
|
-
<p class="
|
|
37
|
+
<p class="text-sm">
|
|
34
38
|
A username is not editable: it is the login, the author URL above, and
|
|
35
39
|
the actor a follower holds. Somebody who needs a different one is a new
|
|
36
40
|
account.
|
|
@@ -39,7 +43,7 @@
|
|
|
39
43
|
purpose: it is what the fediverse already holds, and changing it would
|
|
40
44
|
orphan every follow. #}
|
|
41
45
|
{% if account.storedActorId %}
|
|
42
|
-
<p class="
|
|
46
|
+
<p class="text-sm">
|
|
43
47
|
Also <code>{{ account.storedActorId }}</code> — what the fediverse knows
|
|
44
48
|
this user by. Set by the import or by hand in
|
|
45
49
|
<code>data/users.json</code>, never here.
|
|
@@ -50,16 +54,15 @@
|
|
|
50
54
|
{% if mailConfigured %}
|
|
51
55
|
Optional, and never shown on the public site. It is what a forgotten password is recovered through and where the notices below go. Empty it to stop the site writing to this person at all.
|
|
52
56
|
{% else %}
|
|
53
|
-
Optional, and this site sends no mail yet, so nothing will be sent to one. Choose a provider on <a href="{{ adminUrl }}/settings/email">Settings › Email</a> to turn password recovery on.
|
|
57
|
+
Optional, and this site sends no mail yet, so nothing will be sent to one. Choose a provider on <a class="link" href="{{ adminUrl }}/settings/email">Settings › Email</a> to turn password recovery on.
|
|
54
58
|
{% endif %}
|
|
55
59
|
{% endset %}
|
|
56
60
|
{{ field.text('user-email', fields.email, 'Email address', account.email,
|
|
57
61
|
type='email', autocomplete='email', hint=emailHint) }}
|
|
58
62
|
|
|
59
|
-
<
|
|
60
|
-
<button type="submit">Save email address</button>
|
|
61
|
-
</p>
|
|
63
|
+
<div class="mt-2">{{ button('Save email address', type='submit', color='primary') }}</div>
|
|
62
64
|
</form>
|
|
65
|
+
{% endcall %}
|
|
63
66
|
|
|
64
67
|
{# The public face of this person (TASK-67). Anybody's, not only your own:
|
|
65
68
|
there is one role, so an admin who has just added a colleague can put a
|
|
@@ -67,13 +70,12 @@
|
|
|
67
70
|
sign in. After decision-14 these same fields are what their ActivityPub
|
|
68
71
|
actor carries, which is why they live beside the account rather than in
|
|
69
72
|
the site settings — and why saving them tells their followers. #}
|
|
70
|
-
|
|
73
|
+
{% call card(title='Profile') %}
|
|
74
|
+
<form method="post" action="{{ userProfileUrl }}">
|
|
71
75
|
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
72
76
|
<input type="hidden" name="{{ fields.userId }}" value="{{ account.id }}" />
|
|
73
77
|
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
<p class="admin-hint">
|
|
78
|
+
<p class="text-sm">
|
|
77
79
|
A profile is public. Nothing in it is required — a user with no profile
|
|
78
80
|
still has an archive under their username — and a box left empty is a
|
|
79
81
|
field they do not have.
|
|
@@ -116,7 +118,7 @@
|
|
|
116
118
|
<code>Mastodon | https://example.social/@me | me</code>,
|
|
117
119
|
<code>Their post | https://example.com/post | me nofollow author</code>.
|
|
118
120
|
It is the same line the menu boxes on
|
|
119
|
-
<a href="{{ adminUrl }}/navigation">Navigation</a> take.
|
|
121
|
+
<a class="link" href="{{ adminUrl }}/navigation">Navigation</a> take.
|
|
120
122
|
Every link here is published with <code>rel="me"</code> whether or not
|
|
121
123
|
you type it: it is how Mastodon verifies a profile field that points back
|
|
122
124
|
at this site, and how IndieAuth knows these are yours. Nothing else in
|
|
@@ -125,10 +127,9 @@
|
|
|
125
127
|
{{ field.textarea('profile-links', fields.links, 'Links', account.profile.links, rows='4',
|
|
126
128
|
hint=linksHint, error=profileProblems.links) }}
|
|
127
129
|
|
|
128
|
-
<
|
|
129
|
-
<button type="submit">Save profile</button>
|
|
130
|
-
</p>
|
|
130
|
+
<div class="mt-2">{{ button('Save profile', type='submit', color='primary') }}</div>
|
|
131
131
|
</form>
|
|
132
|
+
{% endcall %}
|
|
132
133
|
|
|
133
134
|
{# Only once there is both somewhere to write to and something to write
|
|
134
135
|
with. These switches decide what is sent to the address above, so without
|
|
@@ -137,10 +138,9 @@
|
|
|
137
138
|
lines below say which is missing where the panel would have been, rather
|
|
138
139
|
than letting it vanish without explanation. #}
|
|
139
140
|
{% if account.email and mailConfigured %}
|
|
140
|
-
|
|
141
|
-
<h2>Email {{ account.username }} about</h2>
|
|
141
|
+
{% call card(title='Email ' + account.username + ' about') %}
|
|
142
142
|
|
|
143
|
-
<p class="
|
|
143
|
+
<p class="text-sm">
|
|
144
144
|
What this site writes to {{ account.username }} at <code>{{ account.email }}</code>.
|
|
145
145
|
</p>
|
|
146
146
|
|
|
@@ -149,17 +149,22 @@
|
|
|
149
149
|
this off" from "left it alone". Adding a notice is one entry in the
|
|
150
150
|
registry and no change here. #}
|
|
151
151
|
{% for notice in account.notifications %}
|
|
152
|
-
<div class="
|
|
153
|
-
<h3>{{ notice.label }}</h3>
|
|
154
|
-
<p class="
|
|
152
|
+
<div class="flex flex-col gap-2 border-t border-base-300 pt-4">
|
|
153
|
+
<h3 class="font-semibold">{{ notice.label }}</h3>
|
|
154
|
+
<p class="text-sm">{{ notice.description }}</p>
|
|
155
155
|
|
|
156
|
-
<form class="
|
|
156
|
+
<form class="flex flex-wrap items-center gap-2" method="post" action="{{ userNotificationsUrl }}">
|
|
157
157
|
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
158
158
|
<input type="hidden" name="{{ fields.userId }}" value="{{ account.id }}" />
|
|
159
159
|
<input type="hidden" name="{{ fields.event }}" value="{{ notice.name }}" />
|
|
160
160
|
<input type="hidden" name="{{ fields.on }}" value="{% if notice.on %}{% else %}1{% endif %}" />
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
{% if notice.on %}
|
|
162
|
+
{{ status('active', 'On') }}
|
|
163
|
+
{{ button('Turn off', type='submit', style='outline', size='xs') }}
|
|
164
|
+
{% else %}
|
|
165
|
+
{{ status('hidden', 'Off') }}
|
|
166
|
+
{{ button('Turn on', type='submit', style='outline', size='xs') }}
|
|
167
|
+
{% endif %}
|
|
163
168
|
</form>
|
|
164
169
|
|
|
165
170
|
{# And, for a notice the registry says can be batched, how often it
|
|
@@ -167,7 +172,7 @@
|
|
|
167
172
|
it and the switch above is a button; the registry decides which
|
|
168
173
|
notices get one. #}
|
|
169
174
|
{% if notice.batched %}
|
|
170
|
-
<form class="
|
|
175
|
+
<form class="flex flex-wrap items-end gap-2" method="post" action="{{ userNotificationModeUrl }}">
|
|
171
176
|
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
172
177
|
<input type="hidden" name="{{ fields.userId }}" value="{{ account.id }}" />
|
|
173
178
|
<input type="hidden" name="{{ fields.event }}" value="{{ notice.name }}" />
|
|
@@ -176,9 +181,9 @@
|
|
|
176
181
|
<option value="{{ choice.value }}"{% if choice.chosen %} selected{% endif %}>{{ choice.label }}</option>
|
|
177
182
|
{% endfor %}
|
|
178
183
|
{% endcall %}
|
|
179
|
-
<
|
|
184
|
+
<div class="pb-1">{{ button('Save', type='submit') }}</div>
|
|
180
185
|
</form>
|
|
181
|
-
<p class="
|
|
186
|
+
<p class="text-sm">
|
|
182
187
|
On an hourly or a daily digest nothing is sent as they arrive;
|
|
183
188
|
instead one message a window lists everything still waiting, and
|
|
184
189
|
no message goes at all when nothing is.
|
|
@@ -186,25 +191,24 @@
|
|
|
186
191
|
{% endif %}
|
|
187
192
|
</div>
|
|
188
193
|
{% endfor %}
|
|
189
|
-
|
|
194
|
+
{% endcall %}
|
|
190
195
|
{% else %}
|
|
191
|
-
|
|
192
|
-
<h2>Email {{ account.username }} about</h2>
|
|
196
|
+
{% call card(title='Email ' + account.username + ' about') %}
|
|
193
197
|
|
|
194
198
|
{% if not mailConfigured %}
|
|
195
|
-
<p class="
|
|
199
|
+
<p class="text-sm">
|
|
196
200
|
Nothing, while this site has no mail provider: there is nothing to send
|
|
197
|
-
with. Choose one on <a href="{{ adminUrl }}/settings/email">Settings
|
|
201
|
+
with. Choose one on <a class="link" href="{{ adminUrl }}/settings/email">Settings
|
|
198
202
|
› Email</a> and what to send comes back here.
|
|
199
203
|
</p>
|
|
200
204
|
{% else %}
|
|
201
|
-
<p class="
|
|
205
|
+
<p class="text-sm">
|
|
202
206
|
Nothing, while the box above is empty: there is nowhere to write to.
|
|
203
207
|
Give {% if account.you %}yourself{% else %}this account{% endif %} an
|
|
204
208
|
address and what to send comes back here.
|
|
205
209
|
</p>
|
|
206
210
|
{% endif %}
|
|
207
|
-
|
|
211
|
+
{% endcall %}
|
|
208
212
|
{% endif %}
|
|
209
213
|
|
|
210
214
|
{# Only on your own page: it asks for the current password, so it is about
|
|
@@ -214,20 +218,22 @@
|
|
|
214
218
|
{# Only on your own page, as the screen it links to only ever lists your
|
|
215
219
|
own connections (TASK-162). #}
|
|
216
220
|
{% if account.you %}
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
<p class="admin-hint">
|
|
221
|
+
{% call card(title='Connected apps') %}
|
|
222
|
+
<p class="text-sm">
|
|
220
223
|
Apps you signed in to with your site's address can act as you.
|
|
221
|
-
<a href="{{ connectedAppsUrl }}">Review or revoke them</a>.
|
|
224
|
+
<a class="link" href="{{ connectedAppsUrl }}">Review or revoke them</a>.
|
|
222
225
|
</p>
|
|
223
|
-
|
|
226
|
+
{% endcall %}
|
|
224
227
|
{% endif %}
|
|
225
228
|
|
|
226
229
|
{% if account.you %}
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
{% include "components/theme-choice.njk" %}
|
|
231
|
+
{% endif %}
|
|
229
232
|
|
|
230
|
-
|
|
233
|
+
{% if account.you %}
|
|
234
|
+
{% call card(title='Change your password') %}
|
|
235
|
+
<form id="change-password" method="post" action="{{ changePasswordUrl }}">
|
|
236
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
231
237
|
|
|
232
238
|
{% if passwordProblems.currentPassword or passwordProblems.newPassword %}
|
|
233
239
|
{% call field.summary('Your password was not changed. Fix the fields marked below and try again.') %}
|
|
@@ -246,31 +252,32 @@
|
|
|
246
252
|
{{ field.password('new-password-confirmation', fields.newPasswordConfirmation,
|
|
247
253
|
'New password again', autocomplete='new-password', required=true) }}
|
|
248
254
|
|
|
249
|
-
<p class="
|
|
255
|
+
<p class="text-sm">Every other browser signed in as you is signed out. This one stays.</p>
|
|
250
256
|
|
|
251
|
-
<
|
|
252
|
-
<button type="submit">Change password</button>
|
|
253
|
-
</p>
|
|
257
|
+
<div class="mt-2">{{ button('Change password', type='submit', color='primary') }}</div>
|
|
254
258
|
</form>
|
|
259
|
+
{% endcall %}
|
|
255
260
|
{% endif %}
|
|
256
261
|
|
|
257
|
-
|
|
258
|
-
<h2>Delete</h2>
|
|
262
|
+
{% call card(title='Delete') %}
|
|
259
263
|
|
|
260
264
|
{# Either the button or the reason there is none, never both: the screen
|
|
261
265
|
does not offer an action it is about to refuse. #}
|
|
262
266
|
{% if deleteRefusal %}
|
|
263
|
-
<p class="
|
|
267
|
+
<p class="text-sm">{{ deleteRefusal }}</p>
|
|
264
268
|
{% else %}
|
|
265
|
-
<p class="
|
|
269
|
+
<p class="text-sm">
|
|
266
270
|
Deleting {{ account.username }} signs out every browser they are in and
|
|
267
271
|
cannot be undone. What they have written stays where it is.
|
|
268
272
|
</p>
|
|
269
273
|
<form method="post" action="{{ deleteUserUrl }}">
|
|
270
274
|
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
271
275
|
<input type="hidden" name="{{ fields.userId }}" value="{{ account.id }}" />
|
|
272
|
-
|
|
276
|
+
{% call cardActions() %}
|
|
277
|
+
{{ button('Delete ' + account.username, type='submit', color='error') }}
|
|
278
|
+
{% endcall %}
|
|
273
279
|
</form>
|
|
274
280
|
{% endif %}
|
|
275
|
-
|
|
281
|
+
{% endcall %}
|
|
282
|
+
</div>
|
|
276
283
|
{% endblock %}
|
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
A listing and nothing else (TASK-97). Every field a user has is edited on
|
|
4
4
|
that user's own screen, which the Edit link in each row goes to, so no box
|
|
5
5
|
in this table can be mistaken for another one. Adding somebody is its own
|
|
6
|
-
screen next door, `new.njk`, because Users > Add new is its own menu entry.
|
|
7
|
-
#}
|
|
6
|
+
screen next door, `new.njk`, because Users > Add new is its own menu entry. #}
|
|
8
7
|
{% extends "layouts/shell.njk" %}
|
|
8
|
+
{% from "components/badge.njk" import badge %}
|
|
9
|
+
{% from "components/button.njk" import button, buttonLink %}
|
|
10
|
+
{% from "components/table.njk" import table %}
|
|
9
11
|
|
|
10
12
|
{% block title %}Users{% endblock %}
|
|
11
13
|
|
|
12
14
|
{% block content %}
|
|
13
|
-
<
|
|
15
|
+
<div class="mb-6 flex flex-wrap items-center gap-4">
|
|
16
|
+
<h1 class="text-2xl font-bold">Users</h1>
|
|
17
|
+
{{ buttonLink('Add new', addUserUrl, color='primary') }}
|
|
18
|
+
</div>
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
<caption class="admin-visually-hidden">Users</caption>
|
|
20
|
+
{% call table('Users') %}
|
|
17
21
|
<thead>
|
|
18
22
|
<tr>
|
|
19
23
|
<th scope="col">Username</th>
|
|
@@ -25,28 +29,26 @@
|
|
|
25
29
|
<tbody>
|
|
26
30
|
{% for entry in users %}
|
|
27
31
|
<tr>
|
|
28
|
-
<td>
|
|
29
|
-
|
|
30
|
-
</td>
|
|
31
|
-
<td>{% if entry.email %}{{ entry.email }}{% else %}<span class="admin-status">None</span>{% endif %}</td>
|
|
32
|
-
<td>{{ entry.createdAt | date('html') }}</td>
|
|
32
|
+
<td><a class="link link-hover font-medium" href="{{ entry.editUrl }}">{{ entry.username }}</a>{% if entry.you %} {{ badge('You') }}{% endif %}</td>
|
|
33
|
+
<td>{{ entry.email or 'None' }}</td>
|
|
34
|
+
<td class="whitespace-nowrap">{{ entry.createdAt | date('html') }}</td>
|
|
33
35
|
{# Edit, then what can be done, then View: the order and the shape
|
|
34
|
-
`pages/documents/list.njk` uses
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
`pages/documents/list.njk` uses. A row here is a row there. #}
|
|
37
|
+
<td>
|
|
38
|
+
<div class="flex items-center gap-1 whitespace-nowrap">
|
|
39
|
+
{{ buttonLink('Edit', entry.editUrl, style='ghost', size='xs') }}
|
|
40
|
+
{% if entry.deletable %}
|
|
41
|
+
<form method="post" action="{{ deleteUserUrl }}">
|
|
42
|
+
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
|
|
43
|
+
<input type="hidden" name="{{ fields.userId }}" value="{{ entry.id }}" />
|
|
44
|
+
{{ button('Delete', type='submit', style='ghost', size='xs') }}
|
|
45
|
+
</form>
|
|
46
|
+
{% endif %}
|
|
47
|
+
{{ buttonLink('View', entry.archiveUrl, style='ghost', size='xs') }}
|
|
48
|
+
</div>
|
|
46
49
|
</td>
|
|
47
50
|
</tr>
|
|
48
51
|
{% endfor %}
|
|
49
52
|
</tbody>
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
{% endcall %}
|
|
52
54
|
{% endblock %}
|