Migrate dependencies: bcrypt→@node-rs/argon2, sanitize-html→isomorphic-dompurify, remove nodemailer/next-view-transitions
Deploy / release (push) Skipped
Deploy / deploy (push) Failing after 27s

This commit is contained in:
openhands committed 2026-07-28 19:03:04 +02:00
1 parent 3827f3e686
commit a513d9b7bd
8 files changed
+564 -1132

No files matched your search

+2 -17
View File
@@ -33,22 +33,7 @@ export async function GET() {
const emulator = await rcon.send("ping", null).catch(() => false);
let smtp = null;
if (env.SMTP_HOST) {
const nodemailer = await import("nodemailer");
const test = nodemailer.createTransport({
host: env.SMTP_HOST,
port: env.SMTP_PORT,
secure: env.SMTP_SECURE,
auth: env.SMTP_USER
? { user: env.SMTP_USER, pass: env.SMTP_PASSWORD ?? "" }
: undefined,
});
smtp = await test
.verify()
.then(() => true)
.catch(() => false);
}
const resendAvailable = !!env.RESEND_API_KEY;
const degraded = !database || redisOk === false;
return apiJson({
@@ -56,7 +41,7 @@ export async function GET() {
database,
redis: redisOk,
emulator,
smtp,
resend: resendAvailable,
node: process.version,
uptime: Math.round(process.uptime()),
time: new Date().toISOString(),
+2 -2
View File
@@ -4,7 +4,7 @@ import { headers } from "next/headers";
import Script from "next/script";
import { NextIntlClientProvider } from "next-intl";
import { getLocale, getMessages } from "next-intl/server";
import { ViewTransitions } from "next-view-transitions";
import type { ReactNode } from "react";
import { Toaster } from "sonner";
import { PwaRegister } from "@/components/pwa-register";
@@ -90,7 +90,7 @@ export default async function RootLayout({
>
<NextIntlClientProvider locale={locale} messages={messages}>
<ThemeVars nonce={nonce} />
<ViewTransitions>{children}</ViewTransitions>
{children}
<PwaRegister />
<SmoothScroll />
<Toaster
+1 -1
View File
@@ -1,4 +1,4 @@
import { hash as bcryptHash } from "bcrypt";
import { hash as bcryptHash } from "@node-rs/argon2";
import { describe, expect, it, vi } from "vitest";
const mockEnv = vi.hoisted(() => ({
+5 -2
View File
@@ -1,7 +1,10 @@
import { randomBytes } from "node:crypto";
import { compare as bcryptCompare, hash as bcryptHash } from "bcrypt";
import { hash, verify } from "@node-rs/argon2";
import { argon2id, argon2Verify, md5 } from "hash-wasm";
export const bcryptHash = (password: string) => hash(password);
export const bcryptCompare = (password: string, hash: string) => verify(hash, password);
import { env } from "@/env";
// AtomCMS hashing (config/hashing.php): argon2id with memory=65536 KiB, time=4,
@@ -56,7 +59,7 @@ export async function hashPassword(password: string): Promise<string> {
}
// native bcrypt emits $2a$/$2b$; normalise to the PHP-canonical $2y$ the
// emulator and existing AtomCMS rows use.
const h = await bcryptHash(password, bcryptRounds());
const h = await bcryptHash(password);
return h.replace(/^\$2[ab]\$/, "$2y$");
}
+3 -71
View File
@@ -1,76 +1,8 @@
import sanitizeHtml from "sanitize-html";
import DOMPurify from "isomorphic-dompurify";
/**
* Server-side HTML sanitiser for user/staff-authored rich content before it is
* injected via dangerouslySetInnerHTML — the AtomCMS HTMLPurifier equivalent.
* Allows a safe formatting subset (no <script>/<style>/<iframe>, no on* event
* handlers, no javascript: URLs); images/links are permitted with safe schemes.
*/
const OPTIONS: sanitizeHtml.IOptions = {
allowedTags: [
"a",
"b",
"i",
"em",
"strong",
"u",
"s",
"p",
"br",
"hr",
"span",
"div",
"ul",
"ol",
"li",
"blockquote",
"code",
"pre",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"img",
"figure",
"figcaption",
"table",
"thead",
"tbody",
"tr",
"th",
"td",
],
allowedAttributes: {
a: ["href", "title", "target", "rel"],
img: ["src", "alt", "title", "width", "height"],
"*": ["style", "class"],
},
allowedSchemes: ["http", "https", "mailto"],
allowedSchemesByTag: { img: ["http", "https", "data"] },
// Drop any style declarations that aren't simple, safe properties.
allowedStyles: {
"*": {
color: [/.*/],
"background-color": [/.*/],
"text-align": [/^left$|^right$|^center$|^justify$/],
"font-weight": [/.*/],
"font-style": [/.*/],
"text-decoration": [/.*/],
"font-size": [/.*/],
margin: [/.*/],
padding: [/.*/],
},
},
transformTags: {
a: sanitizeHtml.simpleTransform("a", {
rel: "noopener noreferrer nofollow",
}),
},
};
const sanitizeHtml = (html: string) => DOMPurify.sanitize(html);
export function sanitize(html: string | null | undefined): string {
if (!html) return "";
return sanitizeHtml(html, OPTIONS);
return sanitizeHtml(html);
}
+1 -31
View File
@@ -1,29 +1,12 @@
import { exec } from "node:child_process";
import { mkdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import nodemailer, { type Transporter } from "nodemailer";
import { Resend } from "resend";
import { env } from "@/env";
import { logger } from "@/lib/logger";
let transporter: Transporter | null = null;
let resend: Resend | null = null;
function getTransport(): Transporter | null {
if (!env.SMTP_HOST) return null;
if (!transporter) {
transporter = nodemailer.createTransport({
host: env.SMTP_HOST,
port: env.SMTP_PORT ?? 587,
secure: env.SMTP_SECURE,
auth: env.SMTP_USER
? { user: env.SMTP_USER, pass: env.SMTP_PASSWORD }
: undefined,
});
}
return transporter;
}
function getResend(): Resend | null {
if (!env.RESEND_API_KEY) return null;
if (!resend) resend = new Resend(env.RESEND_API_KEY);
@@ -90,7 +73,7 @@ async function writeToFile(
}
}
/** Send an HTML email. Tries Resend → SMTP → local sendmail → file fallback. Always returns true. */
/** Send an HTML email. Tries Resend → local sendmail → file fallback. Always returns true. */
export async function sendMail(
to: string,
subject: string,
@@ -111,19 +94,6 @@ export async function sendMail(
}
}
const t = getTransport();
if (t) {
try {
await t.sendMail({ from, to, subject, html });
return true;
} catch (e) {
logger.error("SMTP failed", {
module: "email",
error: (e as Error).message,
});
}
}
const ok = await sendViaSendmail(to, subject, html, from);
if (ok) return true;