Rename service to EngPadSyncService (buf lint), add java_package, add buf.yaml
- Proto service renamed from EngPadSync to EngPadSyncService per buf STANDARD lint rule SERVICE_SUFFIX - Added java_package and java_multiple_files options for Android client - Added buf.yaml with STANDARD lint and FILE breaking detection - Regenerated Go gRPC stubs, updated server references Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
135
web/templates/keys.html
Normal file
135
web/templates/keys.html
Normal file
@@ -0,0 +1,135 @@
|
||||
{{define "title"}}Security Keys — Engineering Pad{{end}}
|
||||
|
||||
{{define "nav-right"}}<a href="/logout">Logout</a>{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<h1>Security Keys</h1>
|
||||
|
||||
<p><a href="/notebooks" class="btn">Back to Notebooks</a></p>
|
||||
|
||||
{{if .Keys}}
|
||||
<table style="width:100%; max-width:600px; border-collapse:collapse; margin:1rem 0;">
|
||||
<thead>
|
||||
<tr style="border-bottom:2px solid #111; text-align:left;">
|
||||
<th style="padding:0.5rem;">Name</th>
|
||||
<th style="padding:0.5rem;">Registered</th>
|
||||
<th style="padding:0.5rem;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Keys}}
|
||||
<tr style="border-bottom:1px solid #ccc;">
|
||||
<td style="padding:0.5rem;">{{.Name}}</td>
|
||||
<td style="padding:0.5rem;">{{.CreatedAt.Format "2006-01-02 15:04"}}</td>
|
||||
<td style="padding:0.5rem;">
|
||||
<form method="POST" action="/keys/delete" style="display:inline;"
|
||||
onsubmit="return confirm('Delete this key?');">
|
||||
<input type="hidden" name="id" value="{{.ID}}">
|
||||
<button type="submit" class="btn" style="color:red; border-color:red;">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p style="margin:1rem 0;">No security keys registered.</p>
|
||||
{{end}}
|
||||
|
||||
{{if .WebAuthnEnabled}}
|
||||
<div style="margin-top:1.5rem;">
|
||||
<h2 style="font-size:1.2rem;">Register New Key</h2>
|
||||
<div class="form-group">
|
||||
<label for="key-name">Key Name</label>
|
||||
<input type="text" id="key-name" placeholder="e.g. YubiKey 5" value="">
|
||||
</div>
|
||||
<button type="button" class="btn" id="register-btn">Register Security Key</button>
|
||||
<p id="register-status" style="margin-top:0.5rem;"></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
if (!window.PublicKeyCredential) {
|
||||
document.getElementById("register-btn").style.display = "none";
|
||||
document.getElementById("register-status").textContent = "WebAuthn is not supported in this browser.";
|
||||
return;
|
||||
}
|
||||
|
||||
var btn = document.getElementById("register-btn");
|
||||
var statusEl = document.getElementById("register-status");
|
||||
|
||||
btn.addEventListener("click", async function() {
|
||||
statusEl.textContent = "";
|
||||
statusEl.style.color = "";
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Waiting for security key...";
|
||||
|
||||
try {
|
||||
var beginResp = await fetch("/webauthn/register/begin", {method: "POST"});
|
||||
if (!beginResp.ok) {
|
||||
var err = await beginResp.json();
|
||||
throw new Error(err.error || "Registration failed");
|
||||
}
|
||||
var options = await beginResp.json();
|
||||
|
||||
// Decode base64url fields.
|
||||
options.publicKey.challenge = base64urlToBuffer(options.publicKey.challenge);
|
||||
options.publicKey.user.id = base64urlToBuffer(options.publicKey.user.id);
|
||||
if (options.publicKey.excludeCredentials) {
|
||||
options.publicKey.excludeCredentials.forEach(function(c) {
|
||||
c.id = base64urlToBuffer(c.id);
|
||||
});
|
||||
}
|
||||
|
||||
var credential = await navigator.credentials.create({publicKey: options.publicKey});
|
||||
|
||||
var body = JSON.stringify({
|
||||
id: credential.id,
|
||||
rawId: bufferToBase64url(credential.rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
attestationObject: bufferToBase64url(credential.response.attestationObject),
|
||||
clientDataJSON: bufferToBase64url(credential.response.clientDataJSON)
|
||||
}
|
||||
});
|
||||
|
||||
var keyName = document.getElementById("key-name").value || "Security Key";
|
||||
var finishResp = await fetch("/webauthn/register/finish?name=" + encodeURIComponent(keyName), {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: body
|
||||
});
|
||||
if (!finishResp.ok) {
|
||||
var err2 = await finishResp.json();
|
||||
throw new Error(err2.error || "Registration failed");
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
} catch(e) {
|
||||
statusEl.style.color = "red";
|
||||
statusEl.textContent = e.message || "Registration failed";
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = "Register Security Key";
|
||||
}
|
||||
});
|
||||
|
||||
function base64urlToBuffer(b64url) {
|
||||
var b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
while (b64.length % 4) b64 += "=";
|
||||
var binary = atob(b64);
|
||||
var bytes = new Uint8Array(binary.length);
|
||||
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
function bufferToBase64url(buf) {
|
||||
var bytes = new Uint8Array(buf);
|
||||
var binary = "";
|
||||
for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user