also looking at this
fix(dashboard): confirm destructive actions that ran on first click
#130
4 files
+99
-10
| @@ -67,6 +67,22 @@ async function revoke(name) { | ||
| 67 | 67 | load() |
| 68 | 68 | } |
| 69 | 69 | |
| 70 | // Removal runs through a typed-confirm bar (same pattern as firewall/secrets): | |
| 71 | // type the username to confirm. Revoking pulls someone's access to every site | |
| 72 | // on the box at once and invalidates their password, so it should never be one | |
| 73 | // misclick away. | |
| 74 | const pendingRevoke = state(null) | |
| 75 | const typedRevoke = state('') | |
| 76 | const canRevoke = derived(() => { const u = pendingRevoke(); return u !== null && typedRevoke() === String(u.username) }) | |
| 77 | function askRevoke(user) { pendingRevoke.set(user); typedRevoke.set('') } | |
| 78 | function cancelRevoke() { pendingRevoke.set(null); typedRevoke.set('') } | |
| 79 | async function confirmRevoke() { | |
| 80 | const u = pendingRevoke() | |
| 81 | if (!u || typedRevoke() !== String(u.username)) return | |
| 82 | pendingRevoke.set(null); typedRevoke.set('') | |
| 83 | await revoke(u.username) | |
| 84 | } | |
| 85 | ||
| 70 | 86 | onMount(() => { load() }) |
| 71 | 87 | </script> |
| 72 | 88 | <!DOCTYPE html> |
| @@ -135,12 +151,18 @@ onMount(() => { load() }) | ||
| 135 | 151 | <td><b>{{ u.name }}</b><span class="mono uname">{{ u.username }}</span></td> |
| 136 | 152 | <td><span class="tag">{{ u.role === 'admin' ? 'box owner' : 'member' }}</span></td> |
| 137 | 153 | <td class="mono">{{ u.role === 'admin' ? 'every site' : siteList(u) }}</td> |
| 138 | <td class="table-actions"><button type="button" class="btn danger sm" @click="revoke(u.username)">Remove</button></td> | |
| 154 | <td class="table-actions"><button type="button" class="btn danger sm" @click="askRevoke(u)">Remove</button></td> | |
| 139 | 155 | </tr> |
| 140 | 156 | </template> |
| 141 | 157 | </tbody> |
| 142 | 158 | </table> |
| 143 | 159 | <div class="compact empty" @show="users().length === 0"><strong>No one invited yet</strong><span>Invite someone above to give them access to a site.</span></div> |
| 160 | <div class="op-confirm" @show="pendingRevoke() !== null" style="margin-top:14px"> | |
| 161 | <span>Type <b class="mono">{{ pendingRevoke()?.username }}</b> to remove <b>{{ pendingRevoke()?.name }}</b> from every site:</span> | |
| 162 | <input class="op-confirm-input" :value="typedRevoke()" @input="typedRevoke.set($event.target.value)" @keydown.enter="confirmRevoke()" placeholder="confirm" autocomplete="off"> | |
| 163 | <button class="btn danger sm" :disabled="!canRevoke()" @click="confirmRevoke()">Remove</button> | |
| 164 | <button class="btn ghost sm" @click="cancelRevoke()">Cancel</button> | |
| 165 | </div> | |
| 144 | 166 | <p class="note">Box owners reach everything on this server. Members reach only the sites listed here, and never the shell, SSH keys, firewall or databases. Access is checked on every request.</p> |
| 145 | 167 | </div> |
| 146 | 168 | </div> |