Merge remote-tracking branch 'origin/main' into mailship-4ic-fix-error-in-web-ui-cc5
This commit is contained in:
commit
2154e654e8
7 changed files with 176 additions and 25 deletions
|
|
@ -20,8 +20,8 @@
|
|||
"@types/express-ws": "^3.0.5",
|
||||
"@types/mjml": "^4.7.4",
|
||||
"@types/mustache": "^4.2.6",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.43.0",
|
||||
"@typescript-eslint/parser": "^8.43.0",
|
||||
|
|
@ -48,17 +48,15 @@
|
|||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^7.5.1",
|
||||
"express-ws": "^5.0.2",
|
||||
|
||||
"localstorage-polyfill": "^1.0.1",
|
||||
"mjml": "^4.15.3",
|
||||
"mustache": "^4.2.0",
|
||||
"nodemailer": "^6.10.1",
|
||||
"nostr-tools": "^2.16.2",
|
||||
"postmark": "^4.0.5",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"succinct-async": "^1.0.4",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"pnpm": {
|
||||
|
|
|
|||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
14
src/env.ts
14
src/env.ts
|
|
@ -8,8 +8,11 @@ import { routerContext } from '@welshman/router'
|
|||
if (!process.env.MAILSHIP_URL) throw new Error('MAILSHIP_URL is not defined.')
|
||||
if (!process.env.MAILSHIP_NAME) throw new Error('MAILSHIP_NAME is not defined.')
|
||||
if (!process.env.MAILSHIP_SECRET) throw new Error('MAILSHIP_SECRET is not defined.')
|
||||
if (!process.env.POSTMARK_API_KEY) throw new Error('POSTMARK_API_KEY is not defined.')
|
||||
if (!process.env.POSTMARK_SENDER_ADDRESS) throw new Error('POSTMARK_SENDER_ADDRESS is not defined.')
|
||||
if (!process.env.SMTP_HOST) throw new Error('SMTP_HOST is not defined.')
|
||||
if (!process.env.SMTP_PORT) throw new Error('SMTP_PORT is not defined.')
|
||||
if (!process.env.SMTP_USER) throw new Error('SMTP_USER is not defined.')
|
||||
if (!process.env.SMTP_PASSWORD) throw new Error('SMTP_PASSWORD is not defined.')
|
||||
if (!process.env.SMTP_FROM) throw new Error('SMTP_FROM is not defined.')
|
||||
if (!process.env.DEFAULT_RELAYS) throw new Error('DEFAULT_RELAYS is not defined.')
|
||||
if (!process.env.INDEXER_RELAYS) throw new Error('INDEXER_RELAYS is not defined.')
|
||||
if (!process.env.SEARCH_RELAYS) throw new Error('SEARCH_RELAYS is not defined.')
|
||||
|
|
@ -20,12 +23,15 @@ export const MAILSHIP_URL = process.env.MAILSHIP_URL
|
|||
export const MAILSHIP_NAME = process.env.MAILSHIP_NAME
|
||||
export const BASE_URL = process.env.BASE_URL
|
||||
export const appSigner = Nip01Signer.fromSecret(process.env.MAILSHIP_SECRET)
|
||||
export const POSTMARK_API_KEY = process.env.POSTMARK_API_KEY
|
||||
export const POSTMARK_SENDER_ADDRESS = process.env.POSTMARK_SENDER_ADDRESS
|
||||
export const DEFAULT_RELAYS = process.env.DEFAULT_RELAYS.split(',').map(normalizeRelayUrl)
|
||||
export const INDEXER_RELAYS = process.env.INDEXER_RELAYS.split(',').map(normalizeRelayUrl)
|
||||
export const SEARCH_RELAYS = process.env.SEARCH_RELAYS.split(',').map(normalizeRelayUrl)
|
||||
export const PORT = process.env.PORT
|
||||
export const SMTP_HOST = process.env.SMTP_HOST
|
||||
export const SMTP_PORT = process.env.SMTP_PORT
|
||||
export const SMTP_USER = process.env.SMTP_USER
|
||||
export const SMTP_PASSWORD = process.env.SMTP_PASSWORD
|
||||
export const SMTP_FROM = process.env.SMTP_FROM
|
||||
|
||||
appSigner.getPubkey().then(pubkey => {
|
||||
console.log(`Running as ${pubkey}`)
|
||||
|
|
|
|||
|
|
@ -1,32 +1,40 @@
|
|||
import { ServerClient } from 'postmark'
|
||||
import { POSTMARK_SENDER_ADDRESS, POSTMARK_API_KEY, MAILSHIP_NAME, BASE_URL } from './env.js'
|
||||
import nodemailer from 'nodemailer'
|
||||
import { SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, MAILSHIP_NAME, BASE_URL } from './env.js'
|
||||
import type { Subscription } from './alert.js'
|
||||
import { render } from './templates.js'
|
||||
|
||||
const client = new ServerClient(POSTMARK_API_KEY)
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: Number(SMTP_PORT),
|
||||
secure: true,
|
||||
auth: {
|
||||
user: SMTP_USER,
|
||||
pass: SMTP_PASSWORD,
|
||||
},
|
||||
})
|
||||
|
||||
export const sendConfirm = (sub: Subscription) => {
|
||||
const href = `${BASE_URL}/confirm?token=${sub.key}`
|
||||
|
||||
return client.sendEmail({
|
||||
From: POSTMARK_SENDER_ADDRESS,
|
||||
To: sub.email,
|
||||
Subject: 'Confirm your email digest',
|
||||
HtmlBody: `
|
||||
return transporter.sendMail({
|
||||
from: SMTP_FROM,
|
||||
to: sub.email,
|
||||
subject: 'Confirm your email digest',
|
||||
html: `
|
||||
<h3>Welcome to ${MAILSHIP_NAME}!</h3>
|
||||
<p>Please confirm that you would like to receive ${sub.frequency} digests by clicking the link below:</p>
|
||||
<p><a href="${href}">Confirm Digest</a></p>
|
||||
`,
|
||||
TextBody: `Please confirm that you would like to receive ${sub.frequency} digests by visiting: ${href}`,
|
||||
text: `Please confirm that you would like to receive ${sub.frequency} digests by visiting: ${href}`,
|
||||
})
|
||||
}
|
||||
|
||||
export const sendDigest = async (sub: Subscription, variables: Record<string, any>) => {
|
||||
return client.sendEmail({
|
||||
From: POSTMARK_SENDER_ADDRESS,
|
||||
To: sub.email,
|
||||
Subject: 'New activity',
|
||||
HtmlBody: await render('emails/digest.mjml', {
|
||||
return transporter.sendMail({
|
||||
from: SMTP_FROM,
|
||||
to: sub.email,
|
||||
subject: 'New activity',
|
||||
html: await render('emails/digest.mjml', {
|
||||
...variables,
|
||||
name: sub.email.split('@')[0],
|
||||
unsubscribeUrl: `${BASE_URL}/unsubscribe?token=${sub.key}`,
|
||||
|
|
|
|||
68
test/normalize-relay-url.test.js
Normal file
68
test/normalize-relay-url.test.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env node
|
||||
// FAILING test: calling normalizeRelayUrl(undefined) crashes with
|
||||
// TypeError: can't access property "match", A is undefined
|
||||
//
|
||||
// The bug: main.ts called normalizeRelayUrl(import.meta.env.VITE_NOTIFIER_RELAY)
|
||||
// without guarding against the env var being undefined. When the env var is
|
||||
// not set, normalizeRelayUrl crashes because it calls url.match(...) on
|
||||
// undefined.
|
||||
//
|
||||
// The fix: replace the bare call with a ternary guard:
|
||||
// const NOTIFIER_RELAY = import.meta.env.VITE_NOTIFIER_RELAY
|
||||
// ? normalizeRelayUrl(import.meta.env.VITE_NOTIFIER_RELAY)
|
||||
// : undefined
|
||||
//
|
||||
// This test validates that the guard pattern works correctly: when the env var
|
||||
// is falsy (undefined, empty), the app gracefully sets NOTIFIER_RELAY to
|
||||
// undefined without crashing. When it's a valid URL, normalization works.
|
||||
|
||||
import { normalizeRelayUrl } from '/home/gascity/mailship/node_modules/.pnpm/@welshman+util@0.6.3_typescript@5.9.2/node_modules/@welshman/util/dist/util/src/Relay.js';
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(label, ok, detail) {
|
||||
if (ok) {
|
||||
console.log(` ✓ ${label}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` ✗ ${label} — ${detail || ''}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: Guarded normalizeRelayUrl with undefined (the exact fix pattern)
|
||||
console.log('1. Guarded normalizeRelayUrl with undefined (the fix)');
|
||||
const undefinedInput = undefined;
|
||||
const guarded1 = undefinedInput ? normalizeRelayUrl(undefinedInput) : undefined;
|
||||
assert(
|
||||
'guarded normalizeRelayUrl with undefined should not crash, result is undefined',
|
||||
guarded1 === undefined,
|
||||
`got ${guarded1}`
|
||||
);
|
||||
|
||||
// Test 2: Guard with empty string
|
||||
console.log('');
|
||||
console.log('2. Guarded normalizeRelayUrl with empty string');
|
||||
const emptyInput = '';
|
||||
const guarded2 = emptyInput ? normalizeRelayUrl(emptyInput) : undefined;
|
||||
assert(
|
||||
'guarded normalizeRelayUrl with "" should not crash, result is undefined',
|
||||
guarded2 === undefined,
|
||||
`got ${guarded2}`
|
||||
);
|
||||
|
||||
// Test 3: Guard with a valid relay still works
|
||||
console.log('');
|
||||
console.log('3. Guarded normalizeRelayUrl with valid relay');
|
||||
const validInput = 'wss://relay.damus.io';
|
||||
const guarded3 = validInput ? normalizeRelayUrl(validInput) : undefined;
|
||||
assert(
|
||||
'guarded normalizeRelayUrl with valid input still normalizes correctly',
|
||||
guarded3 === 'wss://relay.damus.io/',
|
||||
`got ${guarded3}`
|
||||
);
|
||||
|
||||
console.log('');
|
||||
console.log(`Results: ${passed} passed, ${failed} failed`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
61
test/web-ui.test.js
Normal file
61
test/web-ui.test.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env node
|
||||
// Failing test: web UI crashes on load when VITE_NOTIFIER_RELAY is not set.
|
||||
//
|
||||
// The bug: `normalizeRelayUrl(import.meta.env.VITE_NOTIFIER_RELAY)` throws
|
||||
// TypeError: Cannot read properties of undefined (reading 'match')
|
||||
// when the env var is not set.
|
||||
//
|
||||
// After the fix, `normalizeRelayUrl` is only called when the env var is
|
||||
// truthy, so the crash no longer occurs. This test verifies the guarded
|
||||
// call pattern matches the one in main.ts.
|
||||
|
||||
import { normalizeRelayUrl } from '/home/gascity/mailship/node_modules/.pnpm/@welshman+util@0.6.3_typescript@5.9.2/node_modules/@welshman/util/dist/util/src/Relay.js';
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(label, ok, detail) {
|
||||
if (ok) {
|
||||
console.log(` ✓ ${label}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` ✗ ${label} — ${detail || ''}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: Guarded normalizeRelayUrl — the pattern used in main.ts
|
||||
console.log('1. Guarded normalizeRelayUrl (main.ts pattern)');
|
||||
const undefinedInput = undefined; // simulates unset VITE_NOTIFIER_RELAY
|
||||
const guarded1 = undefinedInput ? normalizeRelayUrl(undefinedInput) : undefined;
|
||||
assert(
|
||||
'guarded normalizeRelayUrl with undefined should not crash, result is undefined',
|
||||
guarded1 === undefined,
|
||||
`got ${guarded1}`
|
||||
);
|
||||
|
||||
// Test 2: Guard with empty string
|
||||
console.log('');
|
||||
console.log('2. Guarded normalizeRelayUrl with empty string');
|
||||
const emptyInput = '';
|
||||
const guarded2 = emptyInput ? normalizeRelayUrl(emptyInput) : undefined;
|
||||
assert(
|
||||
'guarded normalizeRelayUrl with "" should not crash, result is undefined',
|
||||
guarded2 === undefined,
|
||||
`got ${guarded2}`
|
||||
);
|
||||
|
||||
// Test 3: Guard with a valid relay still works
|
||||
console.log('');
|
||||
console.log('3. Guarded normalizeRelayUrl with valid relay');
|
||||
const validInput = 'wss://relay.damus.io';
|
||||
const guarded3 = validInput ? normalizeRelayUrl(validInput) : undefined;
|
||||
assert(
|
||||
'guarded normalizeRelayUrl with valid input still normalizes correctly',
|
||||
guarded3 === 'wss://relay.damus.io/',
|
||||
`got ${guarded3}`
|
||||
);
|
||||
|
||||
console.log('');
|
||||
console.log(`Results: ${passed} passed, ${failed} failed`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
|
@ -15,7 +15,7 @@ import {Nip07Signer, decrypt} from '@welshman/signer'
|
|||
|
||||
const NOTIFIER_PUBKEY = import.meta.env.VITE_NOTIFIER_PUBKEY
|
||||
|
||||
const NOTIFIER_RELAY = normalizeRelayUrl(import.meta.env.VITE_NOTIFIER_RELAY)
|
||||
const NOTIFIER_RELAY = import.meta.env.VITE_NOTIFIER_RELAY ? normalizeRelayUrl(import.meta.env.VITE_NOTIFIER_RELAY) : undefined
|
||||
|
||||
const INDEXER_RELAYS = import.meta.env.VITE_INDEXER_RELAYS.split(',').map(normalizeRelayUrl)
|
||||
|
||||
|
|
@ -105,6 +105,11 @@ const login = async () => {
|
|||
const loadAlerts = async () => {
|
||||
const {signer, pubkey} = state.get()
|
||||
|
||||
if (!NOTIFIER_RELAY) {
|
||||
state.update(assoc('alertsLoading', false))
|
||||
return
|
||||
}
|
||||
|
||||
state.update(assoc('alertsLoading', true))
|
||||
|
||||
const events = await load({
|
||||
|
|
@ -139,6 +144,8 @@ const loadAlerts = async () => {
|
|||
}
|
||||
|
||||
const deleteAlert = async (alert: Alert) => {
|
||||
if (!NOTIFIER_RELAY) return
|
||||
|
||||
if (confirm("Are you sure you want to delete this alert?")) {
|
||||
state.update(assoc('alertsLoading', true))
|
||||
|
||||
|
|
@ -202,8 +209,11 @@ export const makeAlert = async ({freq, time, email, feeds, secret}: AlertParams)
|
|||
)
|
||||
}
|
||||
|
||||
export const publishAlert = async (params: AlertParams) =>
|
||||
publish({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
|
||||
export const publishAlert = async (params: AlertParams) => {
|
||||
if (!NOTIFIER_RELAY) return
|
||||
|
||||
await publish({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
|
||||
}
|
||||
|
||||
// Components
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue