import { Hono } from 'hono'; import { basicAuth } from 'hono/basic-auth'; import { DEVICE_NAME, PROTON_IMAP_PASSWORD, PROTON_IMAP_USERNAME } from '@/constants/config'; import { isImapConnected } from '@/modules/proton-mail'; import { checkSignalCli, finishLink, generateLinkQR, getAccount, hasValidAccount, } from '@/modules/signal'; import { getAllMappings, removeEndpoint, updateChannel } from '@/modules/store'; import type { NotificationChannel } from '@/types/notifications'; import { verifyApiKey } from '@/utils/auth'; import { formatPhoneNumber, formatUptime } from '@/utils/format'; import { logError } from '@/utils/log'; let cachedQR: string | null = null; let qrCacheTime = 0; let generatingPromise: Promise | null = null; const QR_CACHE_TTL = 10 * 60 * 1000; export const admin = new Hono(); admin.use( '*', basicAuth({ verifyUser: (_, password, c) => verifyApiKey(password, c), realm: 'SUP Admin - Username: any, Password: API_KEY', }), ); admin.get('/api/health', async (c) => { const signalOk = await checkSignalCli(); const linked = signalOk && (await hasValidAccount()); const hasProtonConfig = PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD; const result: Record = { uptime: formatUptime(process.uptime()), signal: { daemon: signalOk ? 'running' : 'stopped', linked, }, }; if (hasProtonConfig) { result.protonMail = isImapConnected() ? 'connected' : 'disconnected'; } return c.json(result); }); admin.get('/fragment/health', async (c) => { const { html } = await handleHealthFragment(); return c.html(html); }); admin.get('/fragment/signal-info', async (c) => c.html(await handleSignalInfoFragment())); admin.get('/fragment/endpoints', async (c) => c.html(await handleEndpointsFragment())); admin.get('/fragment/link-qr', async (c) => c.html(await handleQRSection())); admin.delete('/action/delete-endpoint', async (c) => { const formData = await c.req.formData(); const endpoint = formData.get('endpoint') as string; if (!endpoint) { return c.text('Invalid endpoint', 400); } removeEndpoint(endpoint); return c.html(await handleEndpointsFragment()); }); admin.post('/action/toggle-channel', async (c) => { try { const formData = await c.req.formData(); const endpoint = formData.get('endpoint') as string; const channel = formData.get('channel') as NotificationChannel; if (!endpoint) { return c.json({ error: 'Invalid endpoint' }, 400); } if (!channel || !['signal', 'unifiedpush'].includes(channel)) { return c.json({ error: 'Invalid channel' }, 400); } updateChannel(endpoint, channel); return c.html(await handleEndpointsFragment()); } catch (err) { logError('Failed to toggle channel:', err); return c.text('Invalid request', 400); } }); const handleHealthFragment = async () => { const signalOk = await checkSignalCli(); const linked = signalOk && (await hasValidAccount()); const imap = isImapConnected(); const hasProtonConfig = PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD; const accountNumber = getAccount(); const html = `
Signal: ${signalOk ? 'Connected' : 'Disconnected'} and ${linked ? 'Linked' : 'Unlinked'} ${linked && accountNumber ? `${formatPhoneNumber(accountNumber)}` : ''}
${ hasProtonConfig ? `
Proton Mail: ${imap ? 'Connected' : 'Disconnected'} ${imap ? `${PROTON_IMAP_USERNAME}` : ''}
` : '' }
${await handleSignalInfoFragment()}
`; return { html, linked }; }; const handleSignalInfoFragment = async () => { if (await hasValidAccount()) { cachedQR = null; return ``; } return handleQRSection(); }; const handleEndpointsFragment = async () => { const endpoints = getAllMappings(); if (endpoints.length === 0) { return '

No endpoints registered

'; } return ` `; }; const handleQRSection = async () => { if (await hasValidAccount()) { return '

Account already linked

'; } const now = Date.now(); if ((!cachedQR || now - qrCacheTime > QR_CACHE_TTL) && !generatingPromise) { generatingPromise = (async () => { try { const qr = await generateLinkQR(); cachedQR = qr; qrCacheTime = Date.now(); finishLink().finally(() => { generatingPromise = null; cachedQR = null; qrCacheTime = 0; }); return qr; } catch (error) { generatingPromise = null; throw error; } })(); } if (generatingPromise && !cachedQR) { try { await generatingPromise; } catch { return '

Signal daemon is starting up, please refresh in a few seconds...

'; } } return `

Scan this QR code with your Signal app:

Settings → Linked Devices → Link New Device

QR Code
`; };