feat: SSH key management on dev pod page — list, add, remove keys
Some checks failed
Build & Release / build (push) Has been cancelled
Some checks failed
Build & Release / build (push) Has been cancelled
This commit is contained in:
@@ -1067,8 +1067,19 @@
|
||||
<span class="stat-value" style="font-family:monospace;font-size:0.8rem">dev</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card-full" id="devpod-keys-card">
|
||||
<div class="card-title">SSH Keys</div>
|
||||
<div id="devpod-keys-list" style="color:#8b949e;font-size:0.845rem">Loading…</div>
|
||||
<div style="display:flex;gap:0.5rem;margin-top:1rem">
|
||||
<input id="devpod-key-input" type="text" placeholder="ssh-ed25519 AAAA… user@host"
|
||||
style="flex:1;background:#161b22;border:1px solid #30363d;border-radius:6px;padding:0.4rem 0.6rem;color:#e6edf3;font-family:monospace;font-size:0.78rem" />
|
||||
<button class="btn btn-primary btn-sm" onclick="addSshKey()">Add Key</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
loadSshKeys();
|
||||
|
||||
// Auto-refresh while starting
|
||||
clearInterval(devpodRefreshTimer);
|
||||
if (state === 'starting') {
|
||||
@@ -1095,6 +1106,60 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSshKeys() {
|
||||
const el = document.getElementById('devpod-keys-list');
|
||||
if (!el) return;
|
||||
try {
|
||||
const res = await apiFetch('/api/devpod/keys');
|
||||
const { keys } = await res.json();
|
||||
renderSshKeys(keys);
|
||||
} catch {
|
||||
if (el) el.innerHTML = '<span style="color:#f85149">Failed to load keys</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderSshKeys(keys) {
|
||||
const el = document.getElementById('devpod-keys-list');
|
||||
if (!el) return;
|
||||
if (!keys.length) { el.innerHTML = '<span style="color:#484f58">No SSH keys configured</span>'; return; }
|
||||
el.innerHTML = keys.map(k => {
|
||||
const parts = k.split(' ');
|
||||
const label = parts.slice(2).join(' ') || parts[0];
|
||||
const short = parts[1] ? parts[1].slice(0, 20) + '…' : k;
|
||||
return `<div style="display:flex;justify-content:space-between;align-items:center;padding:0.35rem 0;border-bottom:1px solid #21262d">
|
||||
<span style="font-family:monospace;font-size:0.78rem;color:#e6edf3">${label} <span style="color:#484f58">${short}</span></span>
|
||||
<button class="btn btn-danger btn-sm" onclick="removeSshKey(${JSON.stringify(k)})">Remove</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function addSshKey() {
|
||||
const input = document.getElementById('devpod-key-input');
|
||||
const key = input.value.trim();
|
||||
if (!key) return;
|
||||
try {
|
||||
const res = await apiFetch('/api/devpod/keys', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ key }) });
|
||||
if (!res.ok) throw new Error((await res.json()).message);
|
||||
const { keys } = await res.json();
|
||||
renderSshKeys(keys);
|
||||
input.value = '';
|
||||
} catch (err) {
|
||||
alert('Failed to add key: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSshKey(key) {
|
||||
if (!confirm('Remove this SSH key?')) return;
|
||||
try {
|
||||
const res = await apiFetch('/api/devpod/keys', { method: 'DELETE', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ key }) });
|
||||
if (!res.ok) throw new Error((await res.json()).message);
|
||||
const { keys } = await res.json();
|
||||
renderSshKeys(keys);
|
||||
} catch (err) {
|
||||
alert('Failed to remove key: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Account ───────────────────────────────────────────────────────────────
|
||||
|
||||
async function changePassword() {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { k8sFetch, rolloutRestart } from "../lib/k8s";
|
||||
import { z } from "zod";
|
||||
import { k8sFetch, rolloutRestart, getSecret, patchSecret } from "../lib/k8s";
|
||||
|
||||
const NAMESPACE = "dev";
|
||||
const DEPLOYMENT = "dev";
|
||||
const SECRET = "dev-secrets";
|
||||
const KEY_FIELD = "ssh-authorized-keys";
|
||||
|
||||
export async function devpodRoutes(app: FastifyInstance) {
|
||||
app.get("/devpod/status", async (_req, reply) => {
|
||||
@@ -63,4 +66,30 @@ export async function devpodRoutes(app: FastifyInstance) {
|
||||
await rolloutRestart(NAMESPACE, DEPLOYMENT);
|
||||
return reply.send({ message: "Dev pod restarting" });
|
||||
});
|
||||
|
||||
app.get("/devpod/keys", async (_req, reply) => {
|
||||
const secrets = await getSecret(NAMESPACE, SECRET);
|
||||
const raw = secrets[KEY_FIELD] ?? "";
|
||||
const keys = raw.split("\n").map(k => k.trim()).filter(Boolean);
|
||||
return reply.send({ keys });
|
||||
});
|
||||
|
||||
app.post("/devpod/keys", async (req, reply) => {
|
||||
const { key } = z.object({ key: z.string().min(10) }).parse(req.body);
|
||||
const secrets = await getSecret(NAMESPACE, SECRET);
|
||||
const existing = (secrets[KEY_FIELD] ?? "").split("\n").map(k => k.trim()).filter(Boolean);
|
||||
if (existing.includes(key.trim())) return reply.send({ keys: existing });
|
||||
const updated = [...existing, key.trim()].join("\n");
|
||||
await patchSecret(NAMESPACE, SECRET, { [KEY_FIELD]: updated });
|
||||
return reply.send({ keys: [...existing, key.trim()] });
|
||||
});
|
||||
|
||||
app.delete("/devpod/keys", async (req, reply) => {
|
||||
const { key } = z.object({ key: z.string().min(10) }).parse(req.body);
|
||||
const secrets = await getSecret(NAMESPACE, SECRET);
|
||||
const existing = (secrets[KEY_FIELD] ?? "").split("\n").map(k => k.trim()).filter(Boolean);
|
||||
const updated = existing.filter(k => k !== key.trim());
|
||||
await patchSecret(NAMESPACE, SECRET, { [KEY_FIELD]: updated.join("\n") });
|
||||
return reply.send({ keys: updated });
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user