domma-cms 0.82.0 → 0.83.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/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/templates/login.html +16 -0
- package/admin/js/views/login.js +2 -2
- package/bin/update.js +7 -3
- package/config/plugins.json +5 -0
- package/package.json +2 -1
- package/plugins/free-tier.lock.json +25 -1
- package/plugins/security/CLAUDE.md +41 -0
- package/plugins/security/admin/lib/blocks.js +46 -0
- package/plugins/security/admin/lib/health.js +148 -0
- package/plugins/security/admin/lib/ip.js +44 -0
- package/plugins/security/admin/lib/lockout.js +78 -0
- package/plugins/security/admin/lib/policy.js +32 -0
- package/plugins/security/admin/lib/probes.js +39 -0
- package/plugins/security/admin/lib/settings.js +86 -0
- package/plugins/security/admin/templates/security.html +170 -0
- package/plugins/security/admin/views/security.js +529 -0
- package/plugins/security/bin/unlock.js +81 -0
- package/plugins/security/config.js +7 -0
- package/plugins/security/plugin.js +303 -0
- package/plugins/security/plugin.json +46 -0
- package/plugins/security/server/common-passwords.NOTICE +3 -0
- package/plugins/security/server/common-passwords.txt +10001 -0
- package/plugins/security/server/facts.js +133 -0
- package/plugins/security/server/state.js +106 -0
- package/plugins/security/tests/security.test.js +134 -0
- package/server/routes/api/auth.js +180 -39
- package/server/routes/api/users.js +20 -0
- package/server/server.js +25 -7
- package/server/services/audit.js +63 -0
- package/server/services/authChecks.js +128 -0
- package/server/services/network.js +49 -0
- package/server/services/plugins.js +5 -1
- package/server/services/sessions.js +171 -0
- package/server/services/siteGitignore.js +1 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<!-- Security. Bound with M.applyBindings - admin/views/security.js inserts the item templates (the router
|
|
2
|
+
renders a templateUrl through _.render, eating any {{…}}). Refresh and Settings are banner actions. -->
|
|
3
|
+
<div class="scx">
|
|
4
|
+
<p class="scx-note is-bad" data-bind-hidden="!error.value" data-bind-text="error.value"></p>
|
|
5
|
+
<p class="scx-note is-warn" data-bind-hidden="checksOk.value">This version of Domma CMS cannot run sign-in checks: lockout and password rules are off until it is updated (0.83 or later). Probe blocking and the health check work.</p>
|
|
6
|
+
|
|
7
|
+
<nav class="scx-tabs" role="tablist" aria-label="Security">
|
|
8
|
+
<button type="button" role="tab" data-on-click="tab_overview" data-bind-class="tab.value === 'overview' && 'is-on'"><span data-icon="shield" data-icon-size="14"></span> Overview</button>
|
|
9
|
+
<button type="button" role="tab" data-on-click="tab_signins" data-bind-class="tab.value === 'signins' && 'is-on'"><span data-icon="log-in" data-icon-size="14"></span> Sign-ins</button>
|
|
10
|
+
<button type="button" role="tab" data-on-click="tab_blocked" data-bind-class="tab.value === 'blocked' && 'is-on'"><span data-icon="x-circle" data-icon-size="14"></span> Blocked <small class="scx-count" data-bind-hidden="!blockedCount.value" data-bind-text="blockedCount.value"></small></button>
|
|
11
|
+
</nav>
|
|
12
|
+
|
|
13
|
+
<!-- Overview -->
|
|
14
|
+
<section class="scx-panel" data-bind-hidden="tab.value !== 'overview'" data-bind-class="loading.value && 'is-busy'">
|
|
15
|
+
<div class="scx-tiles">
|
|
16
|
+
<div class="scx-card scx-tile scx-score" data-bind-data-tone="scoreTone.value">
|
|
17
|
+
<small>Health <span data-help="The share of checks that pass (a check that could be better counts half; one that could not be answered is left out). The checks below say what each looks at and how to fix it."></span></small>
|
|
18
|
+
<strong data-bind-text="scoreText.value">—</strong>
|
|
19
|
+
<span class="scx-bar"><span data-bind-style-width="scoreWidth.value"></span></span>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="scx-card scx-tile"><small>Need attention</small><strong data-bind-text="attentionText.value">—</strong></div>
|
|
22
|
+
<div class="scx-card scx-tile"><small>Failed sign-ins, 24h <span data-help="Failed sign-ins in the last 24 hours: a wrong password, an unknown email, a locked account."></span></small><strong data-bind-text="ov.value.failed">—</strong></div>
|
|
23
|
+
<div class="scx-card scx-tile"><small>Blocked now <span data-help="Addresses blocked by a rule or for probing, and accounts or addresses locked after wrong passwords."></span></small><strong data-bind-text="ov.value.blocked">—</strong></div>
|
|
24
|
+
<div class="scx-card scx-tile"><small>Probes refused today <span data-help="Requests for things no Domma site has - WordPress logins, .env files, database dumps. Scanners send them to every site they find; they are answered with an empty 'not found'."></span></small><strong data-bind-text="ov.value.probes">—</strong></div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="scx-card">
|
|
28
|
+
<div class="scx-card-title">Checks
|
|
29
|
+
<span data-help-title="Checks" data-help="What could let someone in, or let you down. Click a check for why it matters and how to fix it. They are worked out again every ten minutes, or now with Refresh (top right)."></span>
|
|
30
|
+
<span class="scx-spacer"></span>
|
|
31
|
+
<small class="scx-muted" data-bind-text="asOf.value"></small>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="scx-checks" data-each="checks.value key=id"></div>
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
|
|
37
|
+
<!-- Sign-ins -->
|
|
38
|
+
<section class="scx-panel" data-bind-hidden="tab.value !== 'signins'">
|
|
39
|
+
<div class="scx-toolbar">
|
|
40
|
+
<div class="scx-seg" role="group" aria-label="Which sign-ins">
|
|
41
|
+
<button type="button" data-on-click="res_all" data-bind-class="result.value === 'all' && 'is-on'">All</button>
|
|
42
|
+
<button type="button" data-on-click="res_failed" data-bind-class="result.value === 'failed' && 'is-on'">Failed</button>
|
|
43
|
+
<button type="button" data-on-click="res_ok" data-bind-class="result.value === 'ok' && 'is-on'">Succeeded</button>
|
|
44
|
+
</div>
|
|
45
|
+
<select class="form-input scx-days" aria-label="Period" data-model="days.value">
|
|
46
|
+
<option value="1">Last 24 hours</option><option value="7">Last 7 days</option><option value="30">Last 30 days</option><option value="365">All kept</option>
|
|
47
|
+
</select>
|
|
48
|
+
<input type="search" class="form-input scx-search" placeholder="Email, address, browser…" aria-label="Search sign-ins" data-model="query.value">
|
|
49
|
+
<span class="scx-spacer"></span>
|
|
50
|
+
<small class="scx-muted" data-bind-text="signinText.value"></small>
|
|
51
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="exportCsv"><span data-icon="download" data-icon-size="13"></span> CSV</button>
|
|
52
|
+
<span data-help-title="Sign-ins" data-help="Every sign-in attempt, kept for the days set in the settings. Right-click (or ⋮) a row to block its address, unlock the account, or see only that address."></span>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="scx-card scx-flush">
|
|
55
|
+
<p class="scx-note scx-pad" data-bind-hidden="signinRows.value.length" data-bind-text="signinEmpty.value"></p>
|
|
56
|
+
<div class="scx-list scx-signins" data-each="signinRows.value key=id"></div>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<!-- Blocked -->
|
|
61
|
+
<section class="scx-panel" data-bind-hidden="tab.value !== 'blocked'">
|
|
62
|
+
<div class="scx-card">
|
|
63
|
+
<div class="scx-card-title">Locked after wrong passwords
|
|
64
|
+
<span data-help="An account or address is locked for a while after repeated wrong passwords - longer each time. Unlock one here, or with the command line (see Settings) if you are locked out yourself."></span>
|
|
65
|
+
<span class="scx-spacer"></span>
|
|
66
|
+
<button type="button" class="btn btn-xs btn-ghost" data-on-click="unlockAll" data-bind-hidden="!lockRows.value.length" data-bind-disabled="!canManage.value">Unlock all</button>
|
|
67
|
+
</div>
|
|
68
|
+
<p class="scx-note" data-bind-hidden="lockRows.value.length">Nothing is locked.</p>
|
|
69
|
+
<div class="scx-list scx-locks" data-each="lockRows.value key=id"></div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="scx-card">
|
|
72
|
+
<div class="scx-card-title">Blocked for probing
|
|
73
|
+
<span data-help="Addresses that kept asking for things no Domma site has, blocked for the time set in the settings. An address someone signed in from in the last week is never blocked this way."></span></div>
|
|
74
|
+
<p class="scx-note" data-bind-hidden="tempRows.value.length">No address is blocked for probing.</p>
|
|
75
|
+
<div class="scx-list scx-temps" data-each="tempRows.value key=id"></div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="scx-card">
|
|
78
|
+
<div class="scx-card-title">Blocked by hand
|
|
79
|
+
<span data-help="Addresses or ranges that may not reach the site at all - 81.2.69.142, 192.168.1.*, 10.0.0.0/8 or an IPv6 prefix like 2a00:23c7:*. They get a bare 'Forbidden'."></span></div>
|
|
80
|
+
<form class="scx-add" data-on-submit="addRule" data-bind-hidden="!canManage.value">
|
|
81
|
+
<input type="text" class="form-input scx-mono" placeholder="Address or range" aria-label="Address or range" data-model="newRule.value">
|
|
82
|
+
<input type="text" class="form-input" placeholder="Why (optional)" aria-label="Why" data-model="newNote.value">
|
|
83
|
+
<button type="submit" class="btn btn-sm btn-primary" data-bind-disabled="!newRule.value"><span data-icon="plus" data-icon-size="12"></span> Block</button>
|
|
84
|
+
<small class="scx-muted" data-bind-text="youText.value"></small>
|
|
85
|
+
</form>
|
|
86
|
+
<p class="scx-note" data-bind-hidden="ruleRows.value.length">No address is blocked by hand.</p>
|
|
87
|
+
<div class="scx-list scx-rules" data-each="ruleRows.value key=id"></div>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
.scx { display: flex; flex-direction: column; gap: 1rem; }
|
|
94
|
+
.scx [hidden] { display: none !important; }
|
|
95
|
+
.scx-panel { display: flex; flex-direction: column; gap: 1rem; transition: opacity .2s; }
|
|
96
|
+
.scx-panel.is-busy { opacity: .6; }
|
|
97
|
+
.scx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
|
|
98
|
+
.scx-note.is-bad, .scx-note.is-warn { padding: .6rem .8rem; border-radius: var(--dm-radius-md); }
|
|
99
|
+
.scx-note.is-bad { background: color-mix(in srgb, var(--dm-danger) 14%, transparent); color: var(--dm-danger); }
|
|
100
|
+
.scx-note.is-warn { background: color-mix(in srgb, var(--dm-warning) 14%, transparent); color: var(--dm-text); }
|
|
101
|
+
.scx-pad { padding: .8rem .9rem; }
|
|
102
|
+
.scx-tabs { display: flex; gap: .15rem; border-bottom: 1px solid var(--dm-border); overflow-x: auto; }
|
|
103
|
+
.scx-tabs button { border: 0; background: none; color: var(--dm-text-muted); font: inherit; font-size: var(--dm-text-sm); padding: .45rem .75rem; cursor: pointer;
|
|
104
|
+
border-bottom: 2px solid transparent; margin-bottom: -1px; display: inline-flex; align-items: center; gap: .35rem; white-space: nowrap; }
|
|
105
|
+
.scx-tabs button:hover { color: var(--dm-text); }
|
|
106
|
+
.scx-tabs button.is-on { color: var(--dm-text); border-bottom-color: var(--dm-primary); font-weight: 600; }
|
|
107
|
+
.scx-count { font-size: var(--dm-text-xs); padding: 0 .4rem; border-radius: 999px; background: color-mix(in srgb, var(--dm-text) 10%, transparent); }
|
|
108
|
+
.scx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; min-width: 0; }
|
|
109
|
+
.scx-flush { padding: 0; }
|
|
110
|
+
.scx-card-title { display: flex; align-items: center; gap: .4rem; font-weight: 600; flex-wrap: wrap; }
|
|
111
|
+
.scx-spacer { flex: 1; }
|
|
112
|
+
.scx-muted { color: var(--dm-text-muted); font-weight: 400; font-size: var(--dm-text-xs); }
|
|
113
|
+
.scx-mono { font-family: var(--dm-font-mono, monospace); }
|
|
114
|
+
.scx-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr)); gap: 1rem; }
|
|
115
|
+
.scx-tile { gap: .15rem; }
|
|
116
|
+
.scx-tile small { color: var(--dm-text-muted); font-size: var(--dm-text-sm); display: inline-flex; align-items: center; gap: .3rem; }
|
|
117
|
+
.scx-tile strong { font-size: var(--dm-text-2xl, 1.5rem); line-height: 1.15; font-weight: 600; }
|
|
118
|
+
.scx-bar { display: block; height: .35rem; border-radius: 999px; background: color-mix(in srgb, var(--dm-text) 10%, transparent); overflow: hidden; margin-top: .3rem; }
|
|
119
|
+
.scx-bar span { display: block; height: 100%; border-radius: inherit; background: var(--dm-primary); }
|
|
120
|
+
.scx-score[data-tone="good"] .scx-bar span { background: var(--dm-success); }
|
|
121
|
+
.scx-score[data-tone="poor"] .scx-bar span { background: var(--dm-warning); }
|
|
122
|
+
.scx-score[data-tone="bad"] .scx-bar span { background: var(--dm-danger); }
|
|
123
|
+
.scx-checks { display: flex; flex-direction: column; }
|
|
124
|
+
.scx-group { font-size: var(--dm-text-xs); text-transform: uppercase; letter-spacing: .05em; color: var(--dm-text-muted); padding: .7rem .3rem .25rem; font-weight: 600; }
|
|
125
|
+
.scx-check { display: grid; grid-template-columns: 1.6rem minmax(0, 1fr) 1.5rem; gap: .5rem; align-items: start; padding: .5rem .3rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
|
|
126
|
+
.scx-check:hover, .scx-check:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
|
|
127
|
+
.scx-status { display: inline-flex; align-items: center; justify-content: center; width: 1.35rem; height: 1.35rem; border-radius: 50%; font-size: .75rem; font-weight: 700; margin-top: .1rem; }
|
|
128
|
+
.scx-status[data-status="pass"] { color: var(--dm-success); background: color-mix(in srgb, var(--dm-success) 14%, transparent); }
|
|
129
|
+
.scx-status[data-status="warn"] { color: var(--dm-warning); background: color-mix(in srgb, var(--dm-warning) 16%, transparent); }
|
|
130
|
+
.scx-status[data-status="fail"] { color: var(--dm-danger); background: color-mix(in srgb, var(--dm-danger) 14%, transparent); }
|
|
131
|
+
.scx-status[data-status="info"] { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text) 8%, transparent); }
|
|
132
|
+
.scx-check-main { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
|
|
133
|
+
.scx-check-main strong { font-size: var(--dm-text-sm); }
|
|
134
|
+
.scx-check-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
|
|
135
|
+
.scx-more { display: flex; flex-direction: column; gap: .3rem; margin-top: .35rem; font-size: var(--dm-text-sm); }
|
|
136
|
+
.scx-more a { align-self: flex-start; }
|
|
137
|
+
.scx-toolbar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
|
138
|
+
.scx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
|
|
139
|
+
.scx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
|
|
140
|
+
.scx-seg button + button { border-left: 1px solid var(--dm-border); }
|
|
141
|
+
.scx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
|
|
142
|
+
.scx-days, .scx-search { width: auto; padding: .25rem .5rem; font-size: var(--dm-text-sm); }
|
|
143
|
+
.scx-search { flex: 0 1 15rem; }
|
|
144
|
+
.scx-list { display: flex; flex-direction: column; max-height: 34rem; overflow-y: auto; }
|
|
145
|
+
.scx-row { display: grid; align-items: center; gap: .6rem; padding: .45rem .8rem; border-bottom: 1px solid var(--dm-border); font-size: var(--dm-text-sm); cursor: default; }
|
|
146
|
+
.scx-row:last-child { border-bottom: 0; }
|
|
147
|
+
.scx-row:hover, .scx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
|
|
148
|
+
.scx-signins .scx-row { grid-template-columns: 8.5rem 5.5rem minmax(8rem, 1.3fr) 8rem minmax(6rem, 1fr) 1.5rem; }
|
|
149
|
+
.scx-locks .scx-row, .scx-temps .scx-row, .scx-rules .scx-row { grid-template-columns: minmax(8rem, 1fr) minmax(8rem, 1.4fr) 10rem 1.5rem; padding: .45rem .3rem; }
|
|
150
|
+
.scx-cell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
151
|
+
.scx-pill { display: inline-flex; align-items: center; justify-self: start; font-size: var(--dm-text-xs); font-weight: 600; padding: .05rem .5rem; border-radius: 999px; }
|
|
152
|
+
.scx-pill[data-ok="true"] { color: var(--dm-success); background: color-mix(in srgb, var(--dm-success) 14%, transparent); }
|
|
153
|
+
.scx-pill[data-ok="false"] { color: var(--dm-danger); background: color-mix(in srgb, var(--dm-danger) 12%, transparent); }
|
|
154
|
+
.scx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); line-height: 1; color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .25rem; }
|
|
155
|
+
.scx-kebab:hover { color: var(--dm-primary); }
|
|
156
|
+
.scx-add { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; margin: 0; }
|
|
157
|
+
.scx-add .form-input { width: auto; flex: 1 1 11rem; padding: .25rem .5rem; font-size: var(--dm-text-sm); }
|
|
158
|
+
/* settings slideover */
|
|
159
|
+
.scx-form { display: flex; flex-direction: column; gap: 1.1rem; padding-bottom: 1rem; }
|
|
160
|
+
.scx-form h4 { margin: 0 0 .5rem; font-size: var(--dm-text-base); display: flex; align-items: center; gap: .5rem; }
|
|
161
|
+
.scx-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr)); gap: .7rem 1rem; }
|
|
162
|
+
.scx-wide { grid-column: 1 / -1; }
|
|
163
|
+
.scx-err { color: var(--dm-danger); font-size: var(--dm-text-xs); }
|
|
164
|
+
.scx-actions { display: flex; align-items: center; gap: .5rem; border-top: 1px solid var(--dm-border); padding-top: .8rem; }
|
|
165
|
+
.scx-code { font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-xs); background: color-mix(in srgb, var(--dm-text) 7%, transparent); padding: .4rem .55rem; border-radius: var(--dm-radius-sm); white-space: pre-wrap; }
|
|
166
|
+
@media (max-width: 720px) {
|
|
167
|
+
.scx-signins .scx-row { grid-template-columns: 1fr 5rem 1.5rem; }
|
|
168
|
+
.scx-signins .scx-row .scx-hide-sm { display: none; }
|
|
169
|
+
}
|
|
170
|
+
</style>
|