Import pk from camera, QR visibility toggles, docs
CI / test (push) Successful in 5s

- Add Import pk from camera: scan QR → restore pb → auto login → refresh
- Add scanner.js (jsQR) for camera QR decode
- QR visibility: pk shown by default, pb hidden by default (toggles)
- Update docs/frontend-development.md with scanner, Import pk, QR behavior

Made-with: Cursor
This commit is contained in:
2026-03-01 14:05:43 +00:00
parent ef3957b618
commit 8a3cd2c27e
4 changed files with 154 additions and 11 deletions
+66 -1
View File
@@ -1,5 +1,6 @@
import { createApiClient } from "./api.js";
import { toDataURL } from "./qr.js";
import { scanQRFromCamera } from "./scanner.js";
const { createApp, ref, reactive, onMounted, watch } = Vue;
@@ -21,7 +22,11 @@ createApp({
status: "Ready",
qrPk: "",
qrPb: "",
showPrivateQR: false,
showPrivateQR: true,
showPublicQR: false,
showCameraDialog: false,
cameraError: "",
cameraAbortController: null,
});
let client = createApiClient(apiBase.value);
@@ -79,6 +84,59 @@ createApp({
}
};
const closeCameraDialog = () => {
state.showCameraDialog = false;
if (state.cameraAbortController) {
state.cameraAbortController.abort();
state.cameraAbortController = null;
}
state.cameraError = "";
};
const runCameraScan = async () => {
state.cameraError = "";
state.cameraAbortController = new AbortController();
const videoEl = document.getElementById("camera-video");
if (!videoEl) {
state.cameraError = "Video element not found.";
return;
}
try {
const pk = await scanQRFromCamera(videoEl, state.cameraAbortController.signal);
const pkTrimmed = pk.trim();
closeCameraDialog();
state.privateKey = pkTrimmed;
const pb = await client.derivePublicKey(pkTrimmed);
state.publicKey = pb;
client.importKeys({ publicKey: pb, privateKey: pkTrimmed });
await refreshQRCodes();
try {
try {
await client.registerBySigningServiceKey(pb, pkTrimmed);
} catch (err) {
const msg = (err?.message || "").toLowerCase();
const ignore = msg.includes("already registered") || msg.includes("not configured") ||
msg.includes("admin_public_key") || msg.includes("409") || msg.includes("conflict");
if (!ignore) throw err;
}
state.accessToken = await client.loginWithSignature(pb, pkTrimmed);
client.setAccessToken(state.accessToken);
await listCollections();
state.status = "Imported pk from camera, restored pb, logged in.";
} catch (err) {
state.status = `Keys imported. Login failed: ${err.message}`;
}
} catch (err) {
if (err.name === "AbortError") return;
state.cameraError = err.message || "Camera access failed.";
}
};
const importPkFromCamera = () => {
state.showCameraDialog = true;
Vue.nextTick(() => runCameraScan());
};
const register = async () => {
try {
await client.registerBySigningServiceKey(state.publicKey, state.privateKey);
@@ -254,12 +312,18 @@ createApp({
state.showPrivateQR = !state.showPrivateQR;
};
const togglePublicQR = () => {
state.showPublicQR = !state.showPublicQR;
};
return {
apiBase,
state,
rebuildClient,
ensureKeys,
restorePublicKeyFromPrivate,
importPkFromCamera,
closeCameraDialog,
register,
login,
listCollections,
@@ -275,6 +339,7 @@ createApp({
formatFeature,
featuresFor,
togglePrivateQR,
togglePublicQR,
};
},
}).use(Vuetify.createVuetify()).mount("#app");