diff --git a/frontend/index.html b/frontend/index.html
index 8802c86..15c0448 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1067,8 +1067,19 @@
dev
+
+
SSH Keys
+
Loading…
+
+
+
+
+
`;
+ 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 = 'Failed to load keys';
+ }
+ }
+
+ function renderSshKeys(keys) {
+ const el = document.getElementById('devpod-keys-list');
+ if (!el) return;
+ if (!keys.length) { el.innerHTML = 'No SSH keys configured'; 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 `
+ ${label} ${short}
+
+
`;
+ }).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() {
diff --git a/src/routes/devpod.ts b/src/routes/devpod.ts
index 3f09bbf..7dd06c1 100644
--- a/src/routes/devpod.ts
+++ b/src/routes/devpod.ts
@@ -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 });
+ });
}