diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 9787918..0ef68a6 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/app/pages/logout.vue b/app/pages/logout.vue new file mode 100644 index 0000000..5692322 --- /dev/null +++ b/app/pages/logout.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/app/pages/pm.vue b/app/pages/pm.vue index 3e38d48..28f29bf 100644 --- a/app/pages/pm.vue +++ b/app/pages/pm.vue @@ -6,7 +6,7 @@

OpenSquawk

Pilot Monitoring

-

Decision Tree • Enhanced LLM • VATSIM

+

Alpha Build • Decision Tree • VATSIM

diff --git a/nuxt.config.ts b/nuxt.config.ts index af71aa4..f700a73 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -10,6 +10,7 @@ export default defineNuxtConfig({ '@pinia/nuxt', 'nuxt-mongoose', 'nuxt-module-hotjar', + '@nuxt/image', ], aos: {once: true, duration: 600, easing: 'ease-out'}, app: {head: {link: [{rel: 'icon', type: 'image/jpeg', href: '/img/logo.jpeg'}]}}, @@ -55,4 +56,4 @@ export default defineNuxtConfig({ css: [ '~/assets/css/global.css', '~/assets/css/opensquawk-glass.css' ], -}) \ No newline at end of file +}) diff --git a/package.json b/package.json index 0e6d7ab..79a3ab7 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "postinstall": "nuxt prepare" }, "dependencies": { + "@nuxt/image": "1.11.0", "@nuxtjs/tailwindcss": "6.14.0", "@pinia/nuxt": "0.11.2", "dotenv": "^17.2.2", "fluent-ffmpeg": "^2.1.3", + "nodemailer": "^6.9.13", "nuxt": "^4.1.1", "nuxt-aos": "1.2.5", "nuxt-module-hotjar": "1.3.4", diff --git a/public/img/news/alpha-prototype.svg b/public/img/news/alpha-prototype.svg new file mode 100644 index 0000000..5fc8cf7 --- /dev/null +++ b/public/img/news/alpha-prototype.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Alpha Prototype + + + OPEN + SQUAWK + + + + + + + + + + diff --git a/server/utils/notifications.ts b/server/utils/notifications.ts index 2067c5f..9689e78 100644 --- a/server/utils/notifications.ts +++ b/server/utils/notifications.ts @@ -1,24 +1,5 @@ const ADMIN_EMAIL_FALLBACK = 'info@opensquawk.de' -const RESEND_ENDPOINT = 'https://api.resend.com/emails' -const ADMIN_NOTIFICATION_SUBJECT_PREFIX = process.env.NOTIFY_SUBJECT_PREFIX || '[OpenSquawk Web]' - -type NotificationPrimitive = string | number | boolean | Date | null | undefined -type NotificationValue = - | NotificationPrimitive - | NotificationPrimitive[] - | Record - -type NotificationData = Record | Array<[string, NotificationValue]> - -export interface AdminNotificationOptions { - event: string - summary?: string - data?: NotificationData - to?: string - from?: string -} - interface MailOptions { to: string subject: string @@ -30,72 +11,58 @@ interface MailPayload extends MailOptions { from: string } +interface SmtpConfig { + host: string + port: number + secure: boolean + user: string + pass: string +} + function resolveFrom(from?: string) { return from || process.env.NOTIFY_EMAIL_FROM || 'OpenSquawk ' } -async function sendViaResend(payload: MailPayload) { - const apiKey = process.env.NOTIFY_RESEND_API_KEY - if (!apiKey) { - return false +function resolveSmtpConfig(): SmtpConfig | null { + const host = process.env.NOTIFY_SMTP_HOST?.trim() + const user = process.env.NOTIFY_SMTP_USER?.trim() + const pass = process.env.NOTIFY_SMTP_PASS?.trim() + + if (!host || !user || !pass) { + console.warn('SMTP notification is not fully configured. Please set NOTIFY_SMTP_HOST, NOTIFY_SMTP_USER and NOTIFY_SMTP_PASS.') + return null } - try { - const response = await fetch(RESEND_ENDPOINT, { - method: 'POST', - headers: { - Authorization: `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }) + const secure = (process.env.NOTIFY_SMTP_SECURE || '').toLowerCase() === 'true' + const parsedPort = Number.parseInt(process.env.NOTIFY_SMTP_PORT || '', 10) + const port = Number.isNaN(parsedPort) ? (secure ? 465 : 587) : parsedPort - if (!response.ok) { - const errorText = await response.text().catch(() => '') - console.error('Failed to send email via Resend API', response.status, errorText) - return false - } - - return true - } catch (error) { - console.error('Error while sending email via Resend API', error) - return false - } + return { host, user, pass, secure, port } } async function sendViaSmtp(payload: MailPayload) { - const host = process.env.NOTIFY_SMTP_HOST - if (!host) { + const config = resolveSmtpConfig() + if (!config) { return false } - const user = process.env.NOTIFY_SMTP_USER - const pass = process.env.NOTIFY_SMTP_PASS - if (!user || !pass) { - console.warn('SMTP notification is configured without credentials – skipping send.') - return false - } - - const port = Number.parseInt(process.env.NOTIFY_SMTP_PORT || '', 10) - const secure = process.env.NOTIFY_SMTP_SECURE === 'true' - let nodemailer: any = null try { const module = await import('nodemailer') nodemailer = module.default ?? module } catch (error) { - console.warn('nodemailer is not available. Skipping SMTP notification.', error) + console.error('nodemailer is not available. Install the dependency to send SMTP emails.', error) return false } try { const transporter = nodemailer.createTransport({ - host, - port: Number.isNaN(port) ? (secure ? 465 : 587) : port, - secure, + host: config.host, + port: config.port, + secure: config.secure, auth: { - user, - pass, + user: config.user, + pass: config.pass, }, }) @@ -112,143 +79,24 @@ async function sendViaSmtp(payload: MailPayload) { } } -async function sendMailInternal(options: MailOptions) { +export async function sendMail(options: MailOptions) { const payload: MailPayload = { ...options, from: resolveFrom(options.from), } - const sentViaResend = await sendViaResend(payload) - if (sentViaResend) { - return true - } - - const sentViaSmtp = await sendViaSmtp(payload) - if (sentViaSmtp) { - return true - } - - return false -} - -export async function sendMail(options: MailOptions) { - const success = await sendMailInternal(options) + const success = await sendViaSmtp(payload) if (!success) { console.info(`[mail:fallback] ${options.subject}\nEmpfänger: ${options.to}\n${options.text}`) } return success } -function resolveNotificationSubject(event: string) { - const trimmedEvent = event.trim() - if (!trimmedEvent) { - return ADMIN_NOTIFICATION_SUBJECT_PREFIX - } - - if (trimmedEvent.toLowerCase().startsWith(ADMIN_NOTIFICATION_SUBJECT_PREFIX.toLowerCase())) { - return trimmedEvent - } - - return `${ADMIN_NOTIFICATION_SUBJECT_PREFIX} ${trimmedEvent}` -} - -function toNotificationEntries(data?: NotificationData): Array<[string, NotificationValue]> { - if (!data) { - return [] - } - - if (Array.isArray(data)) { - return data - } - - return Object.entries(data) -} - -function formatNotificationValue(value: NotificationValue): string { - if (value === null || value === undefined) { - return '—' - } - - if (Array.isArray(value)) { - const formatted = value - .map((entry) => formatNotificationValue(entry)) - .filter((entry) => entry && entry !== '—') - return formatted.length ? formatted.join(', ') : '—' - } - - if (value instanceof Date) { - return value.toISOString() - } - - if (typeof value === 'object') { - return JSON.stringify(value, null, 2) - } - - if (typeof value === 'boolean') { - return value ? 'Ja' : 'Nein' - } - - if (typeof value === 'string') { - const trimmed = value.trim() - return trimmed ? trimmed : '—' - } - - return String(value) -} - -function formatDataEntries(entries: Array<[string, NotificationValue]>) { - return entries.map(([label, rawValue]) => { - const formattedValue = formatNotificationValue(rawValue) - if (formattedValue.includes('\n')) { - const indented = formattedValue - .split('\n') - .map((line) => ` ${line}`) - .join('\n') - return `• ${label}:\n${indented}` - } - - return `• ${label}: ${formattedValue}` - }) -} - -function buildAdminNotificationText(options: AdminNotificationOptions) { - const lines: string[] = [] - const summary = options.summary?.trim() - if (summary) { - lines.push(summary) - } else { - lines.push(`Neue Aktivität auf der Website: ${options.event}`) - } - - const dataEntries = formatDataEntries([ - ['Ereignis', options.event], - ...toNotificationEntries(options.data), - ]) - - if (dataEntries.length) { - lines.push('') - lines.push('Details:') - lines.push(...dataEntries) - } - - return lines.join('\n') -} - -export async function sendAdminNotification(options: AdminNotificationOptions) { - const to = options.to || process.env.NOTIFY_EMAIL_TO || ADMIN_EMAIL_FALLBACK - const subject = resolveNotificationSubject(options.event) - const text = buildAdminNotificationText(options) - - const success = await sendMailInternal({ - to, - subject, - text, - from: options.from, - }) - +export async function sendAdminNotification(subject: string, text: string) { + const to = process.env.NOTIFY_EMAIL_TO || ADMIN_EMAIL_FALLBACK + const success = await sendMail({ to, subject, text }) if (!success) { console.info(`[notify:fallback] ${subject}\nEmpfänger: ${to}\n${text}`) } - return success } diff --git a/yarn.lock b/yarn.lock index 303b432..4603a37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -772,6 +772,13 @@ __metadata: languageName: node linkType: hard +"@fastify/accept-negotiator@npm:^1.1.0": + version: 1.1.0 + resolution: "@fastify/accept-negotiator@npm:1.1.0" + checksum: 10c0/1cb9a298c992b812869158ddc6093557a877b30e5f77618a7afea985a0667c50bc7113593bf0f7f9dc9b82b94c16e8ab127a0afc3efde6677fd645539f6d08e5 + languageName: node + linkType: hard + "@iconify-json/carbon@npm:^1.1.27": version: 1.2.13 resolution: "@iconify-json/carbon@npm:1.2.13" @@ -1208,7 +1215,7 @@ __metadata: languageName: node linkType: hard -"@nuxt/kit@npm:4.1.2, @nuxt/kit@npm:^4.1.1": +"@nuxt/kit@npm:4.1.2": version: 4.1.2 resolution: "@nuxt/kit@npm:4.1.2" dependencies: @@ -6793,17 +6800,6 @@ __metadata: languageName: node linkType: hard -"nuxt-module-hotjar@npm:1.3.4": - version: 1.3.4 - resolution: "nuxt-module-hotjar@npm:1.3.4" - dependencies: - "@hotjar/browser": "npm:^1.0.9" - "@nuxt/kit": "npm:^4.1.1" - defu: "npm:^6.1.4" - checksum: 10c0/cd4bb0a8b321d96611315681a82eb0e9d896b837540b8cc508cbe4cebd4821641cbcb752ad791675cec5346225431907181165a717e5175d6fa55d4772ced128 - languageName: node - linkType: hard - "nuxt-mongoose@npm:1.0.6": version: 1.0.6 resolution: "nuxt-mongoose@npm:1.0.6" @@ -7062,7 +7058,6 @@ __metadata: fluent-ffmpeg: "npm:^2.1.3" nuxt: "npm:^4.1.1" nuxt-aos: "npm:1.2.5" - nuxt-module-hotjar: "npm:1.3.4" nuxt-mongoose: "npm:1.0.6" openai: "npm:^4.66.0" pinia: "npm:^3.0.3"