bb0540b70c
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
102 lines
3.1 KiB
TypeScript
102 lines
3.1 KiB
TypeScript
import { create } from 'zustand';
|
|
import { api } from '../lib/api.ts';
|
|
|
|
interface PushState {
|
|
isSupported: boolean;
|
|
isSubscribed: boolean;
|
|
permission: NotificationPermission;
|
|
subscribe: () => Promise<void>;
|
|
unsubscribe: () => Promise<void>;
|
|
requestPermission: () => Promise<NotificationPermission>;
|
|
checkSubscription: () => Promise<void>;
|
|
}
|
|
|
|
function urlBase64ToUint8Array(base64String: string): BufferSource {
|
|
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
const rawData = window.atob(base64);
|
|
const outputArray = new Uint8Array(rawData.length);
|
|
for (let i = 0; i < rawData.length; ++i) {
|
|
outputArray[i] = rawData.charCodeAt(i);
|
|
}
|
|
return outputArray;
|
|
}
|
|
|
|
export const usePushStore = create<PushState>((set, get) => ({
|
|
isSupported: 'serviceWorker' in navigator && 'PushManager' in window,
|
|
isSubscribed: false,
|
|
permission: typeof Notification !== 'undefined' ? Notification.permission : 'default',
|
|
|
|
requestPermission: async () => {
|
|
if (!get().isSupported) return 'denied';
|
|
const permission = await Notification.requestPermission();
|
|
set({ permission });
|
|
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-public-key');
|
|
if (!publicKey) return;
|
|
|
|
// Subscribe to push
|
|
const subscription = await registration.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
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', {
|
|
endpoint: sub.endpoint!,
|
|
p256dh: keys.p256dh,
|
|
auth: keys.auth,
|
|
});
|
|
|
|
set({ isSubscribed: true });
|
|
} catch (error) {
|
|
console.error('Failed to subscribe to push:', error);
|
|
}
|
|
},
|
|
|
|
unsubscribe: async () => {
|
|
if (!get().isSupported) return;
|
|
|
|
try {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
const subscription = await registration.pushManager.getSubscription();
|
|
if (subscription) {
|
|
await api.post('/push/unsubscribe', { endpoint: subscription.endpoint });
|
|
await subscription.unsubscribe();
|
|
}
|
|
set({ isSubscribed: false });
|
|
} catch (error) {
|
|
console.error('Failed to unsubscribe from push:', error);
|
|
}
|
|
},
|
|
}));
|