add push settings
This commit is contained in:
32
apps/frontend/lib/firebase-config.ts
Normal file
32
apps/frontend/lib/firebase-config.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export interface FirebasePublicConfig {
|
||||
enabled: boolean;
|
||||
projectId: string;
|
||||
apiKey: string;
|
||||
appId: string;
|
||||
messagingSenderId: string;
|
||||
vapidKey: string;
|
||||
}
|
||||
|
||||
export function parseFirebasePublicConfig(settings: Record<string, string>): FirebasePublicConfig | null {
|
||||
const enabled = ['true', '1', 'yes'].includes((settings.FIREBASE_PUSH_ENABLED ?? '').trim().toLowerCase());
|
||||
if (!enabled) return null;
|
||||
|
||||
const projectId = settings.FIREBASE_PROJECT_ID?.trim() ?? '';
|
||||
const apiKey = settings.FIREBASE_WEB_API_KEY?.trim() ?? '';
|
||||
const appId = settings.FIREBASE_WEB_APP_ID?.trim() ?? '';
|
||||
const messagingSenderId = settings.FIREBASE_WEB_MESSAGING_SENDER_ID?.trim() ?? '';
|
||||
const vapidKey = settings.FIREBASE_WEB_VAPID_KEY?.trim() ?? '';
|
||||
|
||||
if (!projectId || !apiKey || !appId || !messagingSenderId || !vapidKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
projectId,
|
||||
apiKey,
|
||||
appId,
|
||||
messagingSenderId,
|
||||
vapidKey
|
||||
};
|
||||
}
|
||||
112
apps/frontend/lib/firebase-push.ts
Normal file
112
apps/frontend/lib/firebase-push.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
'use client';
|
||||
|
||||
import { initializeApp, getApps, type FirebaseApp } from 'firebase/app';
|
||||
import { getMessaging, getToken, isSupported, onMessage, type Messaging } from 'firebase/messaging';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import type { FirebasePublicConfig } from '@/lib/firebase-config';
|
||||
|
||||
const SW_PATH = '/firebase-messaging-sw.js';
|
||||
const REGISTERED_TOKEN_KEY = 'lendry_fcm_token';
|
||||
|
||||
let messagingInstance: Messaging | null = null;
|
||||
|
||||
function getFirebaseApp(config: FirebasePublicConfig): FirebaseApp {
|
||||
const existing = getApps()[0];
|
||||
if (existing) return existing;
|
||||
|
||||
return initializeApp({
|
||||
apiKey: config.apiKey,
|
||||
authDomain: `${config.projectId}.firebaseapp.com`,
|
||||
projectId: config.projectId,
|
||||
messagingSenderId: config.messagingSenderId,
|
||||
appId: config.appId
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureServiceWorker() {
|
||||
if (!('serviceWorker' in navigator)) return null;
|
||||
const registration = await navigator.serviceWorker.register(SW_PATH, { scope: '/' });
|
||||
await navigator.serviceWorker.ready;
|
||||
return registration;
|
||||
}
|
||||
|
||||
export async function registerFirebasePush(authToken: string, config: FirebasePublicConfig) {
|
||||
if (typeof window === 'undefined') return null;
|
||||
if (!config.enabled) return null;
|
||||
|
||||
const supported = await isSupported();
|
||||
if (!supported) return null;
|
||||
|
||||
if (!('Notification' in window)) return null;
|
||||
|
||||
const permission = Notification.permission === 'granted'
|
||||
? 'granted'
|
||||
: await Notification.requestPermission();
|
||||
|
||||
if (permission !== 'granted') return null;
|
||||
|
||||
const registration = await ensureServiceWorker();
|
||||
if (!registration) return null;
|
||||
|
||||
const app = getFirebaseApp(config);
|
||||
messagingInstance = getMessaging(app);
|
||||
|
||||
const fcmToken = await getToken(messagingInstance, {
|
||||
vapidKey: config.vapidKey,
|
||||
serviceWorkerRegistration: registration
|
||||
});
|
||||
|
||||
if (!fcmToken) return null;
|
||||
|
||||
const previousToken = window.localStorage.getItem(REGISTERED_TOKEN_KEY);
|
||||
if (previousToken && previousToken !== fcmToken) {
|
||||
try {
|
||||
await apiFetch('/notifications/push/register', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ token: previousToken })
|
||||
}, authToken);
|
||||
} catch {
|
||||
// ignore stale unregister errors
|
||||
}
|
||||
}
|
||||
|
||||
await apiFetch('/notifications/push/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
token: fcmToken,
|
||||
platform: 'WEB',
|
||||
deviceLabel: navigator.userAgent.slice(0, 120)
|
||||
})
|
||||
}, authToken);
|
||||
|
||||
window.localStorage.setItem(REGISTERED_TOKEN_KEY, fcmToken);
|
||||
return fcmToken;
|
||||
}
|
||||
|
||||
export function subscribeFirebaseForegroundMessages(listener: (payload: { title: string; message: string; data?: Record<string, string> }) => void) {
|
||||
if (!messagingInstance) return () => undefined;
|
||||
|
||||
return onMessage(messagingInstance, (payload) => {
|
||||
listener({
|
||||
title: payload.notification?.title ?? payload.data?.title ?? 'Уведомление',
|
||||
message: payload.notification?.body ?? payload.data?.message ?? '',
|
||||
data: payload.data
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function unregisterFirebasePush(authToken: string | null) {
|
||||
const stored = typeof window !== 'undefined' ? window.localStorage.getItem(REGISTERED_TOKEN_KEY) : null;
|
||||
if (!stored || !authToken) return;
|
||||
|
||||
try {
|
||||
await apiFetch('/notifications/push/register', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ token: stored })
|
||||
}, authToken);
|
||||
} catch {
|
||||
// ignore logout cleanup errors
|
||||
} finally {
|
||||
window.localStorage.removeItem(REGISTERED_TOKEN_KEY);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export const SYSTEM_SETTING_GROUPS: Record<string, string> = {
|
||||
auth: 'Аутентификация и регистрация',
|
||||
ldap: 'LDAP / Active Directory',
|
||||
messaging: 'Email и SMS',
|
||||
firebase: 'Firebase Push',
|
||||
limits: 'Лимиты и ограничения',
|
||||
media: 'Медиа и файлы'
|
||||
};
|
||||
@@ -166,6 +167,69 @@ export const SYSTEM_SETTING_CATALOG: SystemSettingMeta[] = [
|
||||
{ key: 'SMS_LOGIN', label: 'Логин smsc.ru', group: 'messaging', type: 'text', showWhen: { key: 'SMS_ENABLED', equals: 'true' } },
|
||||
{ key: 'SMS_PASSWORD', label: 'Пароль smsc.ru', group: 'messaging', type: 'text', showWhen: { key: 'SMS_ENABLED', equals: 'true' } },
|
||||
{ key: 'SMS_SENDER', label: 'Имя отправителя SMS', group: 'messaging', type: 'text', hint: 'Опционально, если одобрено провайдером', showWhen: { key: 'SMS_ENABLED', equals: 'true' } },
|
||||
{
|
||||
key: 'FIREBASE_PUSH_ENABLED',
|
||||
label: 'Push-уведомления включены',
|
||||
group: 'firebase',
|
||||
type: 'boolean',
|
||||
hint: 'Отправка push через Firebase Cloud Messaging на зарегистрированные устройства'
|
||||
},
|
||||
{
|
||||
key: 'FIREBASE_PROJECT_ID',
|
||||
label: 'Firebase Project ID',
|
||||
group: 'firebase',
|
||||
type: 'text',
|
||||
hint: 'project_id из консоли Firebase и JSON сервисного аккаунта',
|
||||
showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' }
|
||||
},
|
||||
{
|
||||
key: 'FIREBASE_CLIENT_EMAIL',
|
||||
label: 'Service Account Email',
|
||||
group: 'firebase',
|
||||
type: 'text',
|
||||
hint: 'client_email из JSON ключа сервисного аккаунта',
|
||||
showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' }
|
||||
},
|
||||
{
|
||||
key: 'FIREBASE_PRIVATE_KEY',
|
||||
label: 'Service Account Private Key',
|
||||
group: 'firebase',
|
||||
type: 'textarea',
|
||||
hint: 'private_key из JSON. Вставьте как есть — переносы строк сохранятся',
|
||||
showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' }
|
||||
},
|
||||
{
|
||||
key: 'FIREBASE_WEB_API_KEY',
|
||||
label: 'Web API Key',
|
||||
group: 'firebase',
|
||||
type: 'text',
|
||||
hint: 'Публичный ключ для клиентского Firebase SDK (веб и PWA)',
|
||||
showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' }
|
||||
},
|
||||
{
|
||||
key: 'FIREBASE_WEB_APP_ID',
|
||||
label: 'Web App ID',
|
||||
group: 'firebase',
|
||||
type: 'text',
|
||||
hint: 'appId веб-приложения из консоли Firebase',
|
||||
showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' }
|
||||
},
|
||||
{
|
||||
key: 'FIREBASE_WEB_MESSAGING_SENDER_ID',
|
||||
label: 'Messaging Sender ID',
|
||||
group: 'firebase',
|
||||
type: 'text',
|
||||
hint: 'senderId для FCM в браузере',
|
||||
showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' }
|
||||
},
|
||||
{
|
||||
key: 'FIREBASE_WEB_VAPID_KEY',
|
||||
label: 'Web Push VAPID Key',
|
||||
group: 'firebase',
|
||||
type: 'text',
|
||||
hint: 'Пара ключей Web Push certificates → Key pair из Firebase Cloud Messaging',
|
||||
showWhen: { key: 'FIREBASE_PUSH_ENABLED', equals: 'true' }
|
||||
},
|
||||
{ key: 'MAX_FAMILY_MEMBERS', label: 'Макс. участников семьи', group: 'limits', type: 'number' },
|
||||
{ key: 'MAX_ACTIVE_SESSIONS', label: 'Макс. активных сессий', group: 'limits', type: 'number' },
|
||||
{ key: 'AVATAR_URL_TTL_MINUTES', label: 'TTL ссылки на аватар', group: 'media', type: 'number', unit: 'мин' }
|
||||
@@ -189,6 +253,8 @@ function parseBooleanValue(value: string) {
|
||||
|
||||
export const MESSAGING_SETTING_KEYS = SYSTEM_SETTING_CATALOG.filter((item) => item.group === 'messaging').map((item) => item.key);
|
||||
|
||||
export const FIREBASE_SETTING_KEYS = SYSTEM_SETTING_CATALOG.filter((item) => item.group === 'firebase').map((item) => item.key);
|
||||
|
||||
export const EMAIL_SETTING_KEYS = SYSTEM_SETTING_CATALOG.filter((item) =>
|
||||
item.key.startsWith('EMAIL_')
|
||||
).map((item) => item.key);
|
||||
|
||||
Reference in New Issue
Block a user