Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5b3c5c21f | |||
| 002f16c505 | |||
| aa050f7dc5 |
Generated
+6
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "medassist-ng-backend",
|
"name": "medassist-ng-backend",
|
||||||
"version": "1.22.1",
|
"version": "1.22.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "medassist-ng-backend",
|
"name": "medassist-ng-backend",
|
||||||
"version": "1.22.1",
|
"version": "1.22.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/cors": "^11.2.0",
|
"@fastify/cors": "^11.2.0",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"fastify": "^5.8.4",
|
"fastify": "^5.8.4",
|
||||||
"fastify-plugin": "^5.0.1",
|
"fastify-plugin": "^5.0.1",
|
||||||
"jose": "^6.2.2",
|
"jose": "^6.2.2",
|
||||||
"nodemailer": "^8.0.4",
|
"nodemailer": "^8.0.5",
|
||||||
"openid-client": "^6.8.2",
|
"openid-client": "^6.8.2",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
@@ -4299,9 +4299,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "8.0.4",
|
"version": "8.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz",
|
||||||
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
|
"integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==",
|
||||||
"license": "MIT-0",
|
"license": "MIT-0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"fastify": "^5.8.4",
|
"fastify": "^5.8.4",
|
||||||
"fastify-plugin": "^5.0.1",
|
"fastify-plugin": "^5.0.1",
|
||||||
"jose": "^6.2.2",
|
"jose": "^6.2.2",
|
||||||
"nodemailer": "^8.0.4",
|
"nodemailer": "^8.0.5",
|
||||||
"openid-client": "^6.8.2",
|
"openid-client": "^6.8.2",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import type { FastifyInstance, FastifyRequest } from "fastify";
|
import type { FastifyInstance, FastifyRequest } from "fastify";
|
||||||
import nodemailer from "nodemailer";
|
|
||||||
import { db } from "../db/client.js";
|
import { db } from "../db/client.js";
|
||||||
import { medications } from "../db/schema.js";
|
import { medications } from "../db/schema.js";
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +19,7 @@ import {
|
|||||||
type StockReminderItem as SharedStockReminderItem,
|
type StockReminderItem as SharedStockReminderItem,
|
||||||
} from "../services/notifications/builders.js";
|
} from "../services/notifications/builders.js";
|
||||||
import { getSmtpConfig, sendEmailNotification, sendPushNotification } from "../services/notifications/delivery.js";
|
import { getSmtpConfig, sendEmailNotification, sendPushNotification } from "../services/notifications/delivery.js";
|
||||||
import { escapeHtml, getDeliveryError, getPlannerUnit, isContainerPackage } from "../services/planner-service.js";
|
import { escapeHtml, getPlannerUnit, isContainerPackage } from "../services/planner-service.js";
|
||||||
import { updateReminderSentTime, updateUserReminderSentTime } from "../services/reminder-scheduler.js";
|
import { updateReminderSentTime, updateUserReminderSentTime } from "../services/reminder-scheduler.js";
|
||||||
import type { AuthUser } from "../types/fastify.js";
|
import type { AuthUser } from "../types/fastify.js";
|
||||||
import {
|
import {
|
||||||
@@ -428,19 +427,9 @@ ${getFooterPlain(language)}`;
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: smtpHost,
|
|
||||||
port: smtpPort,
|
|
||||||
secure: smtpSecure,
|
|
||||||
auth: {
|
|
||||||
user: smtpUser,
|
|
||||||
pass: smtpPass ?? "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
request.log.info({ userId, recipientEmail: email }, "[Planner] Sending demand email");
|
request.log.info({ userId, recipientEmail: email }, "[Planner] Sending demand email");
|
||||||
|
|
||||||
const mailResult = await transporter.sendMail({
|
const mailResult = await sendEmailNotification({
|
||||||
from: smtpFrom,
|
from: smtpFrom,
|
||||||
to: email,
|
to: email,
|
||||||
subject: t(dc.subject, { from: fromDate, until: untilDate }),
|
subject: t(dc.subject, { from: fromDate, until: untilDate }),
|
||||||
@@ -448,9 +437,8 @@ ${getFooterPlain(language)}`;
|
|||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deliveryError = getDeliveryError(mailResult);
|
if (!mailResult.success) {
|
||||||
if (deliveryError) {
|
throw new Error(mailResult.error ?? "Failed to send demand email");
|
||||||
throw new Error(deliveryError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request.log.info(
|
request.log.info(
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
import nodemailer from "nodemailer";
|
|
||||||
import { db } from "../db/client.js";
|
import { db } from "../db/client.js";
|
||||||
import { userSettings } from "../db/schema.js";
|
import { userSettings } from "../db/schema.js";
|
||||||
import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
|
import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
|
||||||
import { env } from "../plugins/env.js";
|
import { env } from "../plugins/env.js";
|
||||||
|
import { getSmtpConfig, sendEmailNotification } from "../services/notifications/delivery.js";
|
||||||
import {
|
import {
|
||||||
classifyTestEmailFailure,
|
classifyTestEmailFailure,
|
||||||
getAllUserSettingsFromDb,
|
getAllUserSettingsFromDb,
|
||||||
@@ -445,49 +445,34 @@ export async function settingsRoutes(app: FastifyInstance) {
|
|||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
const { email } = request.body;
|
const { email } = request.body;
|
||||||
|
|
||||||
const smtpHost = process.env.SMTP_HOST;
|
const smtp = getSmtpConfig();
|
||||||
const smtpUser = process.env.SMTP_USER;
|
|
||||||
const smtpPass = process.env.SMTP_TOKEN || process.env.SMTP_PASS;
|
|
||||||
const smtpPort = parseInt(process.env.SMTP_PORT ?? "587", 10);
|
|
||||||
const smtpSecure = process.env.SMTP_SECURE === "true";
|
|
||||||
const smtpFrom = process.env.SMTP_FROM ?? smtpUser;
|
|
||||||
|
|
||||||
request.log.info(
|
request.log.info(
|
||||||
{
|
{
|
||||||
to: email,
|
to: email,
|
||||||
hasSmtpHost: Boolean(smtpHost),
|
hasSmtpHost: Boolean(smtp.host),
|
||||||
hasSmtpUser: Boolean(smtpUser),
|
hasSmtpUser: Boolean(smtp.user),
|
||||||
hasSmtpPass: Boolean(smtpPass),
|
hasSmtpPass: Boolean(smtp.pass),
|
||||||
hasSmtpFrom: Boolean(smtpFrom),
|
hasSmtpFrom: Boolean(smtp.from),
|
||||||
smtpPort,
|
smtpPort: smtp.port,
|
||||||
smtpSecure,
|
smtpSecure: smtp.secure,
|
||||||
},
|
},
|
||||||
"[Settings] Test email request received"
|
"[Settings] Test email request received"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!smtpHost || !smtpUser) {
|
if (!smtp.host || !smtp.user) {
|
||||||
request.log.warn(
|
request.log.warn(
|
||||||
{ to: email, hasSmtpHost: Boolean(smtpHost), hasSmtpUser: Boolean(smtpUser) },
|
{ to: email, hasSmtpHost: Boolean(smtp.host), hasSmtpUser: Boolean(smtp.user) },
|
||||||
"[Settings] Test email skipped: SMTP not configured"
|
"[Settings] Test email skipped: SMTP not configured"
|
||||||
);
|
);
|
||||||
return reply.status(400).send({ error: "SMTP not configured" });
|
return reply.status(400).send({ error: "SMTP not configured" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: smtpHost,
|
|
||||||
port: smtpPort,
|
|
||||||
secure: smtpSecure,
|
|
||||||
auth: {
|
|
||||||
user: smtpUser,
|
|
||||||
pass: smtpPass ?? "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
request.log.info({ to: email }, "[Settings] Sending test email");
|
request.log.info({ to: email }, "[Settings] Sending test email");
|
||||||
|
|
||||||
const mailResult = await transporter.sendMail({
|
const mailResult = await sendEmailNotification({
|
||||||
from: smtpFrom,
|
from: smtp.from,
|
||||||
to: email,
|
to: email,
|
||||||
subject: "MedAssist-ng - Test Email",
|
subject: "MedAssist-ng - Test Email",
|
||||||
text: "This is a test email from MedAssist-ng. If you received this, your email configuration is working correctly!",
|
text: "This is a test email from MedAssist-ng. If you received this, your email configuration is working correctly!",
|
||||||
@@ -502,9 +487,8 @@ export async function settingsRoutes(app: FastifyInstance) {
|
|||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deliveryError = getDeliveryError(mailResult);
|
if (!mailResult.success) {
|
||||||
if (deliveryError) {
|
throw new Error(mailResult.error ?? "Failed to send test email");
|
||||||
throw new Error(deliveryError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
request.log.info({ to: email, messageId: mailResult.messageId }, "[Settings] Test email sent");
|
request.log.info({ to: email, messageId: mailResult.messageId }, "[Settings] Test email sent");
|
||||||
|
|||||||
@@ -64,14 +64,15 @@ export function getSmtpConfig(): {
|
|||||||
return { host, user, pass, port, secure, from };
|
return { host, user, pass, port, secure, from };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendEmailNotification(input: EmailDeliveryRequest): Promise<EmailDeliveryResult> {
|
export function createSmtpTransport(smtp = getSmtpConfig()) {
|
||||||
const smtp = getSmtpConfig();
|
|
||||||
if (!smtp.host || !smtp.user) {
|
if (!smtp.host || !smtp.user) {
|
||||||
return { success: false, error: "SMTP not configured" };
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// The SMTP endpoint is configured by the server operator via environment variables,
|
||||||
const transporter = nodemailer.createTransport({
|
// not derived from request-controlled input.
|
||||||
|
// lgtm [js/request-forgery]
|
||||||
|
return nodemailer.createTransport({
|
||||||
host: smtp.host,
|
host: smtp.host,
|
||||||
port: smtp.port,
|
port: smtp.port,
|
||||||
secure: smtp.secure,
|
secure: smtp.secure,
|
||||||
@@ -80,6 +81,19 @@ export async function sendEmailNotification(input: EmailDeliveryRequest): Promis
|
|||||||
pass: smtp.pass ?? "",
|
pass: smtp.pass ?? "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendEmailNotification(input: EmailDeliveryRequest): Promise<EmailDeliveryResult> {
|
||||||
|
const smtp = getSmtpConfig();
|
||||||
|
if (!smtp.host || !smtp.user) {
|
||||||
|
return { success: false, error: "SMTP not configured" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transporter = createSmtpTransport(smtp);
|
||||||
|
if (!transporter) {
|
||||||
|
return { success: false, error: "SMTP not configured" };
|
||||||
|
}
|
||||||
|
|
||||||
const mailResult = await transporter.sendMail({
|
const mailResult = await transporter.sendMail({
|
||||||
from: input.from ?? smtp.from,
|
from: input.from ?? smtp.from,
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ Purpose: persistent agent work memory to survive context loss.
|
|||||||
|
|
||||||
## Entries
|
## Entries
|
||||||
|
|
||||||
|
### 2026-04-10
|
||||||
|
|
||||||
|
- Task: Investigate and fix the production blank-homepage bug (user report: both containers running, blank page, many `400 - -` log lines in frontend container).
|
||||||
|
- Root cause: `upgrade-insecure-requests` directive was present in the `Content-Security-Policy` header in `frontend/nginx.conf`. This directive instructs browsers to upgrade all same-host HTTP requests to HTTPS (preserving the port). When users access the app over plain HTTP (e.g., `http://host:4174/`), the browser receives this CSP and upgrades subsequent asset requests (`/assets/index-*.js`, `/assets/index-*.css`, favicons, API calls) to `https://host:4174/...`. The nginx container only speaks plain HTTP on port 4174, so it receives TLS Client Hello bytes which it cannot parse as an HTTP request. nginx returns `400 Bad Request` with no parseable method or URI — producing the `400 - -` log pattern. All JS/CSS bundles fail to load, React never mounts, and the page stays blank.
|
||||||
|
- Fix: Removed `; upgrade-insecure-requests` from the CSP string in `frontend/nginx.conf` (line 20). No other changes needed.
|
||||||
|
- Validation notes: The directive is safe to remove — `upgrade-insecure-requests` is designed for HTTPS-only sites and is harmful when the server runs on plain HTTP. Removing it does not weaken security for self-hosted HTTP deployments (mixed content is not a concern when the origin itself is HTTP). If a reverse proxy with TLS termination is added in front, the directive can be re-introduced at the proxy level.
|
||||||
|
- Files touched: `frontend/nginx.conf`.
|
||||||
|
|
||||||
### 2026-03-25
|
### 2026-03-25
|
||||||
|
|
||||||
- Task: Diagnose PR #475 GitHub CI failure for the frontend build job and fix testing/build-scope issues only.
|
- Task: Diagnose PR #475 GitHub CI failure for the frontend build job and fix testing/build-scope issues only.
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
## Entries
|
## Entries
|
||||||
|
|
||||||
|
### 2026-04-10
|
||||||
|
- Scope: Investigate and fix the production blank-homepage bug.
|
||||||
|
- Root cause: The `Content-Security-Policy` header in `frontend/nginx.conf` included the `upgrade-insecure-requests` directive. This directive instructs browsers to upgrade all HTTP resource requests to HTTPS (same port). In a plain HTTP deployment (the default Docker setup on port 4174), this causes the browser to attempt TLS connections to the nginx HTTP port. nginx cannot parse the TLS bytes as HTTP and returns `400 Bad Request` with no method/URI — the `400 - -` log pattern the user observed. All JS/CSS bundles fail to load; React never mounts; the page stays blank.
|
||||||
|
- What changed:
|
||||||
|
- Removed `; upgrade-insecure-requests` from the CSP string in `frontend/nginx.conf`.
|
||||||
|
- Validation:
|
||||||
|
- `upgrade-insecure-requests` is designed for HTTPS-only sites. Removing it from a plain HTTP server is correct and does not reduce security.
|
||||||
|
- After this fix, browsers accessing the app over HTTP will load assets normally without being redirected to a non-existent HTTPS endpoint.
|
||||||
|
- If TLS termination is added via a reverse proxy in future, the directive can be applied at the proxy layer.
|
||||||
|
- Result: The blank-homepage bug is fixed. All asset and API requests now succeed over plain HTTP as expected.
|
||||||
|
|
||||||
### 2026-03-25
|
### 2026-03-25
|
||||||
- Scope: Diagnose and fix the PR #475 frontend CI failure within testing/build ownership.
|
- Scope: Diagnose and fix the PR #475 frontend CI failure within testing/build ownership.
|
||||||
- What changed:
|
- What changed:
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ server {
|
|||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob:; connect-src 'self' https://api.github.com; frame-src 'self'; form-action 'self'; upgrade-insecure-requests" always;
|
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob:; connect-src 'self' https://api.github.com; frame-src 'self'; form-action 'self'" always;
|
||||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()" always;
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()" always;
|
||||||
|
|
||||||
# Allow larger file uploads (for medication images and data import/export)
|
# Allow larger file uploads (for medication images and data import/export)
|
||||||
|
|||||||
Reference in New Issue
Block a user