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
+27 -3
View File
@@ -8,6 +8,7 @@ interface PushState {
subscribe: () => Promise<void>;
unsubscribe: () => Promise<void>;
requestPermission: () => Promise<NotificationPermission>;
checkSubscription: () => Promise<void>;
}
function urlBase64ToUint8Array(base64String: string): BufferSource {
@@ -24,7 +25,7 @@ function urlBase64ToUint8Array(base64String: string): BufferSource {
export const usePushStore = create<PushState>((set, get) => ({
isSupported: 'serviceWorker' in navigator && 'PushManager' in window,
isSubscribed: false,
permission: 'default',
permission: typeof Notification !== 'undefined' ? Notification.permission : 'default',
requestPermission: async () => {
if (!get().isSupported) return 'denied';
@@ -33,16 +34,31 @@ export const usePushStore = create<PushState>((set, get) => ({
return permission;
},
checkSubscription: async () => {
if (!get().isSupported) return;
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
set({ isSubscribed: !!subscription });
} catch {
// ignore
}
},
subscribe: async () => {
if (!get().isSupported) return;
try {
// Request notification permission first
const permission = await get().requestPermission();
if (permission !== 'granted') return;
// Register service worker
const registration = await navigator.serviceWorker.register('/sw.js');
await navigator.serviceWorker.ready;
// Get VAPID public key from server
const { publicKey } = await api.get<{ publicKey: string }>('/push/vapid-key');
const { publicKey } = await api.get<{ publicKey: string }>('/push/vapid-public-key');
if (!publicKey) return;
// Subscribe to push
@@ -51,8 +67,16 @@ export const usePushStore = create<PushState>((set, get) => ({
applicationServerKey: urlBase64ToUint8Array(publicKey),
});
const sub = subscription.toJSON();
const keys = sub.keys as { p256dh: string; auth: string };
// Send subscription to server
await api.post('/push/subscribe', subscription.toJSON());
await api.post('/push/subscribe', {
endpoint: sub.endpoint!,
p256dh: keys.p256dh,
auth: keys.auth,
});
set({ isSubscribed: true });
} catch (error) {
console.error('Failed to subscribe to push:', error);