Finish Phase 6: Push frontend, WebAuthn frontend, OpenAPI/Swagger

Frontend:
- stores/push.ts: fixed VAPID key endpoint, added checkSubscription, proper subscription format
- UserSettings.tsx: added [ENABLE PUSH] toggle and [REGISTER PASSKEY] button
- LoginForm.tsx: added [SIGN IN WITH PASSKEY] button

Backend:
- cmd/server/main.go: added Swagger annotations, /docs/* route
- docs/: generated swagger.json, swagger.yaml, docs.go
- Makefile: added docs target for swag init
This commit is contained in:
2026-06-28 18:53:05 -04:00
parent 4a9e0c1f76
commit bb0540b70c
10 changed files with 285 additions and 4 deletions
+36
View File
@@ -85,6 +85,42 @@ export function LoginForm() {
{isLoading ? '[PROCESSING...]' : isRegister ? '[REGISTER]' : '[LOGIN]'}
</button>
</form>
{!isRegister && (
<div className="mt-3">
<button
type="button"
onClick={async () => {
try {
// Get challenge from server
const resp = await fetch('/api/v1/auth/webauthn/login/begin', { method: 'POST', credentials: 'include' });
if (!resp.ok) throw new Error('Passkey login not available');
const options = await resp.json();
// Use WebAuthn API
const credential = await navigator.credentials.get({ publicKey: options });
if (!credential) return;
// Send to server
const finishResp = await fetch('/api/v1/auth/webauthn/login/finish', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
});
if (finishResp.ok) {
navigate('/');
}
} catch (err) {
console.error('Passkey login failed:', err);
}
}}
className="terminal-button w-full border-gb-aqua text-gb-aqua"
>
[SIGN IN WITH PASSKEY]
</button>
</div>
)}
<div className="mt-4 text-center">
<button
type="button"
+68 -1
View File
@@ -1,9 +1,11 @@
import { useState, useRef, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuthStore } from '../stores/auth.ts';
import { usePushStore } from '../stores/push.ts';
export function UserSettings() {
const { user, updateProfile, isLoading, error, clearError, fetchMe } = useAuthStore();
const { isSupported, isSubscribed, subscribe, unsubscribe, checkSubscription } = usePushStore();
const navigate = useNavigate();
const [displayName, setDisplayName] = useState('');
@@ -22,7 +24,8 @@ export function UserSettings() {
setStatusText(user.status_text || '');
setAccentColor(user.accent_color || '#fe8019');
}
}, [user]);
checkSubscription();
}, [user, checkSubscription]);
if (!user) {
navigate('/login');
@@ -231,6 +234,70 @@ export function UserSettings() {
</div>
</div>
{/* Push Notifications */}
{isSupported && (
<div className="border border-gb-bg-t p-4">
<label className="block text-gb-fg-f mb-2 font-mono text-sm">
NOTIFICATIONS:
</label>
<div className="flex items-center gap-3">
<button
type="button"
onClick={isSubscribed ? unsubscribe : subscribe}
className={`terminal-button text-xs ${isSubscribed ? 'border-gb-green text-gb-green' : 'border-gb-orange text-gb-orange'}`}
>
{isSubscribed ? '[DISABLE PUSH]' : '[ENABLE PUSH]'}
</button>
<span className="text-gb-fg-f text-xs font-mono">
{isSubscribed ? '● subscribed' : '○ not subscribed'}
</span>
</div>
<p className="text-gb-fg-f text-xs font-mono mt-2">
Receive push notifications for @mentions and DMs when the app is closed.
</p>
</div>
)}
{/* Passkey Registration */}
<div className="border border-gb-bg-t p-4">
<label className="block text-gb-fg-f mb-2 font-mono text-sm">
PASSKEYS:
</label>
<button
type="button"
onClick={async () => {
try {
const resp = await fetch('/api/v1/auth/webauthn/register/begin', { method: 'POST', credentials: 'include' });
if (!resp.ok) throw new Error('Passkey registration not available');
const options = await resp.json();
const credential = await navigator.credentials.create({ publicKey: options });
if (!credential) return;
const finishResp = await fetch('/api/v1/auth/webauthn/register/finish', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
});
if (finishResp.ok) {
alert('Passkey registered successfully!');
}
} catch (err) {
console.error('Passkey registration failed:', err);
alert('Passkey registration not yet available on this server.');
}
}}
className="terminal-button text-xs border-gb-aqua text-gb-aqua"
>
[REGISTER PASSKEY]
</button>
<p className="text-gb-fg-f text-xs font-mono mt-2">
Use biometrics or security key for passwordless login.
</p>
</div>
{/* Errors & Success */}
{error && (
<p className="text-gb-red text-sm font-mono">ERR: {error}</p>