Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/ui/pages/partials/op-confirm.stx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<div class="op-confirm panel" @show="pending() !== null">
<span>Type <b class="mono">{{ confirmTok() }}</b> to {{ confirmVerb() }}:</span>
<input class="op-confirm-input" :value="typed()" @input="typed.set($event.target.value)" @keydown.enter="runOp()" placeholder="confirm" autocomplete="off">
<button class="btn sm" :disabled="!canRun()" @click="runOp()">Run</button>
<button class="btn ghost sm" @click="cancelOp()">Cancel</button>
<input class="op-confirm-input" :value="typed()" @input="typed.set($event.target.value)" @keydown.enter="runOp()" placeholder="confirm" autocomplete="off" aria-label="Type the confirmation token to proceed">
<button type="button" class="btn sm{{ (danger ?? false) ? ' danger' : '' }}" :disabled="!canRun()" @click="runOp()">{{ label ?? 'Run' }}</button>
<button type="button" class="btn ghost sm" @click="cancelOp()">Cancel</button>
</div>
@if (output ?? true)
<pre class="action-output op-output" @show="opShown()">{{ opOutput() }}</pre>
@endif
17 changes: 7 additions & 10 deletions packages/ui/pages/server/firewall.stx
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ async function removePort(port) {
setMessage('Removed port ' + port + ' from cloud config.' + applyLine(data.apply), 'ok')
}

// Remove runs through a typed-confirm bar: type the port number to confirm.
// Remove runs through the shared typed-confirm bar: type the port number to
// confirm. Names match the contract partials/op-confirm.stx binds to.
const pending = state(null); const typed = state('')
const confirmTok = derived(() => { const p = pending(); return p ? String(p) : '' })
const confirmTok = derived(() => { const p = pending(); return p !== null ? String(p) : '' })
const confirmVerb = derived(() => { const p = pending(); return p !== null ? 'remove port ' + p : '' })
const canRun = derived(() => { const p = pending(); return p !== null && typed() === String(p) })
function askRemove(port) { pending.set(port); typed.set('') }
function cancelRemove() { pending.set(null); typed.set('') }
async function runRemove() {
function cancelOp() { pending.set(null); typed.set('') }
async function runOp() {
const p = pending(); if (p === null || typed() !== String(p)) return
pending.set(null); typed.set('')
await removePort(p)
Expand Down Expand Up @@ -120,12 +122,7 @@ onMount(() => { load() })
</template>
</div>
<div class="compact empty" @show="ports().length === 0"><strong>No extra ports configured</strong><span>Only 22, 80, and 443 are open. Allow a port above to update <span class="mono">infrastructure.compute.firewall</span>.</span></div>
<div class="op-confirm panel" @show="pending() !== null">
<span>Type <b class="mono">{{ confirmTok() }}</b> to remove this port:</span>
<input class="op-confirm-input" :value="typed()" @input="typed.set($event.target.value)" @keydown.enter="runRemove()" placeholder="confirm" autocomplete="off">
<button class="btn sm" :disabled="!canRun()" @click="runRemove()">Run</button>
<button class="btn ghost sm" @click="cancelRemove()">Cancel</button>
</div>
@include('../partials/op-confirm', { label: 'Remove', danger: true, output: false })
<p class="note">Ports are declarative: this page edits <span class="mono">infrastructure.compute.firewall</span> in cloud config and applies <span class="mono">ufw allow</span> / <span class="mono">ufw delete allow</span> live on the box. The full config is reconciled again on the next <span class="mono">cloud deploy</span>.</p>
</div>
</div>
Expand Down
27 changes: 12 additions & 15 deletions packages/ui/pages/server/ssh-keys.stx
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,17 @@ async function removeKey(n) {
// Removal runs through a typed-confirm bar (same pattern as firewall/team):
// type the key name to confirm. Removing the wrong key can lock every operator
// out of the box, so it should never be one misclick away.
const pendingRemove = state(null)
const typedRemove = state('')
const canRemove = derived(() => { const k = pendingRemove(); return k !== null && typedRemove() === String(k.name) })
function askRemove(key) { pendingRemove.set(key); typedRemove.set('') }
function cancelRemove() { pendingRemove.set(null); typedRemove.set('') }
async function confirmRemove() {
const k = pendingRemove()
if (!k || typedRemove() !== String(k.name)) return
pendingRemove.set(null); typedRemove.set('')
const pending = state(null)
const typed = state('')
const canRun = derived(() => { const k = pending(); return k !== null && typed() === String(k.name) })
const confirmTok = derived(() => { const k = pending(); return k ? String(k.name) : '' })
const confirmVerb = derived(() => 'remove this key from the box')
function askRemove(key) { pending.set(key); typed.set('') }
function cancelOp() { pending.set(null); typed.set('') }
async function runOp() {
const k = pending()
if (!k || typed() !== String(k.name)) return
pending.set(null); typed.set('')
await removeKey(k.name)
}

Expand Down Expand Up @@ -100,12 +102,7 @@ onMount(() => { load() })
</tbody>
</table>
<div class="compact empty" @show="keys().length === 0"><strong>No SSH keys configured</strong><span>Add a public key above to update <span class="mono">infrastructure.compute.sshKeys</span>.</span></div>
<div class="op-confirm" @show="pendingRemove() !== null" style="margin-top:14px">
<span>Type <b class="mono">{{ pendingRemove()?.name }}</b> to remove this key from the box:</span>
<input class="op-confirm-input" :value="typedRemove()" @input="typedRemove.set($event.target.value)" @keydown.enter="confirmRemove()" placeholder="confirm" autocomplete="off" aria-label="Type the key name to confirm removal">
<button type="button" class="btn danger sm" :disabled="!canRemove()" @click="confirmRemove()">Remove</button>
<button type="button" class="btn ghost sm" @click="cancelRemove()">Cancel</button>
</div>
@include('../partials/op-confirm', { label: 'Remove', danger: true, output: false })
<p class="note">Keys are declarative: this page edits <span class="mono">infrastructure.compute.sshKeys</span> in cloud config. Run <span class="mono">cloud deploy</span> to apply changes to the box. Connect with <span class="mono">cloud server:ssh {{ server.name }}</span>.</p>
</div>
</div>
Expand Down
35 changes: 16 additions & 19 deletions packages/ui/pages/server/team.stx
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,21 @@ async function revoke(name) {
load()
}

// Removal runs through a typed-confirm bar (same pattern as firewall/secrets):
// type the username to confirm. Revoking pulls someone's access to every site
// on the box at once and invalidates their password, so it should never be one
// misclick away.
const pendingRevoke = state(null)
const typedRevoke = state('')
const canRevoke = derived(() => { const u = pendingRevoke(); return u !== null && typedRevoke() === String(u.username) })
function askRevoke(user) { pendingRevoke.set(user); typedRevoke.set('') }
function cancelRevoke() { pendingRevoke.set(null); typedRevoke.set('') }
async function confirmRevoke() {
const u = pendingRevoke()
if (!u || typedRevoke() !== String(u.username)) return
pendingRevoke.set(null); typedRevoke.set('')
// Removal runs through the shared typed-confirm bar: type the username to
// confirm. Revoking pulls someone's access to every site on the box at once and
// invalidates their password, so it should never be one misclick away. Names
// match the contract partials/op-confirm.stx binds to.
const pending = state(null)
const typed = state('')
const confirmTok = derived(() => { const u = pending(); return u ? String(u.username) : '' })
const confirmVerb = derived(() => { const u = pending(); return u ? 'remove ' + u.name + ' from every site' : '' })
const canRun = derived(() => { const u = pending(); return u !== null && typed() === String(u.username) })
function askRevoke(user) { pending.set(user); typed.set('') }
function cancelOp() { pending.set(null); typed.set('') }
async function runOp() {
const u = pending()
if (!u || typed() !== String(u.username)) return
pending.set(null); typed.set('')
await revoke(u.username)
}

Expand Down Expand Up @@ -157,12 +159,7 @@ onMount(() => { load() })
</tbody>
</table>
<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>
<div class="op-confirm" @show="pendingRevoke() !== null" style="margin-top:14px">
<span>Type <b class="mono">{{ pendingRevoke()?.username }}</b> to remove <b>{{ pendingRevoke()?.name }}</b> from every site:</span>
<input class="op-confirm-input" :value="typedRevoke()" @input="typedRevoke.set($event.target.value)" @keydown.enter="confirmRevoke()" placeholder="confirm" autocomplete="off">
<button class="btn danger sm" :disabled="!canRevoke()" @click="confirmRevoke()">Remove</button>
<button class="btn ghost sm" @click="cancelRevoke()">Cancel</button>
</div>
@include('../partials/op-confirm', { label: 'Remove', danger: true, output: false })
<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>
</div>
</div>
Expand Down
27 changes: 12 additions & 15 deletions packages/ui/pages/serverless/alarms.stx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ const msgOk = state(false)
const busy = state(false)

// Delete flow uses a typed-confirm (the alarm name) before firing.
const pendingDelete = state(null)
const typedDelete = state('')
const canDelete = derived(() => { const p = pendingDelete(); return !!p && typedDelete() === p })
const pending = state(null)
const typed = state('')
const canRun = derived(() => { const p = pending(); return !!p && typed() === p })
const confirmTok = derived(() => pending() ?? '')
const confirmVerb = derived(() => 'delete this alarm')

function toneOf(s) { return s === 'ALARM' ? 'bad' : (s === 'OK' ? 'ok' : 'warn') }

Expand Down Expand Up @@ -60,12 +62,12 @@ async function createAlarm() {
finally { busy.set(false) }
}

function askDelete(name) { pendingDelete.set(name); typedDelete.set('') }
function cancelDelete() { pendingDelete.set(null); typedDelete.set('') }
async function confirmDelete() {
const name = pendingDelete()
if (!name || typedDelete() !== name) return
pendingDelete.set(null); typedDelete.set('')
function askDelete(name) { pending.set(name); typed.set('') }
function cancelOp() { pending.set(null); typed.set('') }
async function runOp() {
const name = pending()
if (!name || typed() !== name) return
pending.set(null); typed.set('')
msg.set('Deleting ' + name + '...'); msgOk.set(false)
try {
const res = await fetch('/api/serverless/alarms', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name, confirm: name }) })
Expand Down Expand Up @@ -115,12 +117,7 @@ async function confirmDelete() {
</tbody>
</table>

<div class="op-confirm" @show="pendingDelete() !== null" style="margin-top:14px">
<span>Type <b class="mono">{{ pendingDelete() }}</b> to delete this alarm:</span>
<input class="op-confirm-input" :value="typedDelete()" @input="typedDelete.set($event.target.value)" @keydown.enter="confirmDelete()" placeholder="confirm" autocomplete="off">
<button class="btn danger sm" :disabled="!canDelete()" @click="confirmDelete()">Delete</button>
<button class="btn ghost sm" @click="cancelDelete()">Cancel</button>
</div>
@include('../partials/op-confirm', { label: 'Delete', danger: true, output: false })
</div>
</div>

Expand Down
27 changes: 12 additions & 15 deletions packages/ui/pages/serverless/secrets.stx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ const msgOk = state(false)
const busy = state(false)

// Delete flow uses a typed-confirm (the secret id) before firing.
const pendingDelete = state(null)
const typedDelete = state('')
const canDelete = derived(() => { const p = pendingDelete(); return !!p && typedDelete() === p })
const pending = state(null)
const typed = state('')
const canRun = derived(() => { const p = pending(); return !!p && typed() === p })
const confirmTok = derived(() => pending() ?? '')
const confirmVerb = derived(() => 'delete this secret')

async function refresh() {
try { const r = await fetch('/api/serverless/secrets'); if (!r.ok) return; const x = await r.json(); if (Array.isArray(x.secrets)) rows.set(x.secrets) } catch {}
Expand All @@ -45,12 +47,12 @@ async function setSecret() {
finally { busy.set(false) }
}
function pickSource(src) { secretId.set(src) }
function askDelete(src) { pendingDelete.set(src); typedDelete.set('') }
function cancelDelete() { pendingDelete.set(null); typedDelete.set('') }
async function confirmDelete() {
const id = pendingDelete()
if (!id || typedDelete() !== id) return
pendingDelete.set(null); typedDelete.set('')
function askDelete(src) { pending.set(src); typed.set('') }
function cancelOp() { pending.set(null); typed.set('') }
async function runOp() {
const id = pending()
if (!id || typed() !== id) return
pending.set(null); typed.set('')
msg.set('Deleting ' + id + '...'); msgOk.set(false)
try {
const res = await fetch('/api/serverless/secrets', { method: 'DELETE', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ secretId: id, confirm: id }) })
Expand Down Expand Up @@ -101,12 +103,7 @@ async function confirmDelete() {
</tbody>
</table>

<div class="op-confirm" @show="pendingDelete() !== null" style="margin-top:14px">
<span>Type <b class="mono">{{ pendingDelete() }}</b> to delete this secret:</span>
<input class="op-confirm-input" :value="typedDelete()" @input="typedDelete.set($event.target.value)" @keydown.enter="confirmDelete()" placeholder="confirm" autocomplete="off">
<button class="btn danger sm" :disabled="!canDelete()" @click="confirmDelete()">Delete</button>
<button class="btn ghost sm" @click="cancelDelete()">Cancel</button>
</div>
@include('../partials/op-confirm', { label: 'Delete', danger: true, output: false })
</div>
</div>

Expand Down