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:
2026-03-24 21:25:09 -07:00
parent 4dc71703fe
commit 49de9269d6
11 changed files with 509 additions and 170 deletions

135
web/templates/keys.html Normal file
View 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}}