Migrate dependencies: bcrypt→@node-rs/argon2, sanitize-html→isomorphic-dompurify, remove nodemailer/next-view-transitions
This commit is contained in:
1 parent
3827f3e686
commit
a513d9b7bd
8 files changed
+564
-1132
No files matched your search
+5
-20
@@ -11,7 +11,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"prisma:generate": "prisma generate",
|
||||
"typecheck": "tsc6 --noEmit --incremental false",
|
||||
"typecheck": "tsc --noEmit --incremental",
|
||||
"biome:check": "biome check --write .",
|
||||
"biome:lint": "biome lint .",
|
||||
"biome:format": "biome format --write .",
|
||||
@@ -19,8 +19,6 @@
|
||||
"analyze": "ANALYZE=true pnpm build",
|
||||
"test": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"lint": "eslint . --ext .ts,.tsx --max-warnings=50",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix --max-warnings=50",
|
||||
"lhci:collect": "lhci collect",
|
||||
"lhci:assert": "lhci assert",
|
||||
"lhci:server": "lhci server",
|
||||
@@ -31,8 +29,7 @@
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"biome check --write",
|
||||
"eslint --fix --max-warnings=50"
|
||||
"biome check --write"
|
||||
],
|
||||
"*.{json,md,css,scss,html}": [
|
||||
"biome format --write"
|
||||
@@ -44,17 +41,18 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hookform/resolvers": "^5.5.7",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@prisma/adapter-mariadb": "^7.9.1",
|
||||
"@prisma/client": "^7.9.1",
|
||||
"@sentry/nextjs": "^10.68.0",
|
||||
"@tanstack/react-virtual": "^3.14.8",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"croner": "^10.0.1",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"ioredis": "^5.11.1",
|
||||
"isomorphic-dompurify": "^1.2.4",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
@@ -67,15 +65,12 @@
|
||||
"next": "^16.2.12",
|
||||
"next-auth": "5.0.0-beta.32",
|
||||
"next-intl": "^4.13.4",
|
||||
"next-view-transitions": "^0.3.5",
|
||||
"nodemailer": "^9.0.3",
|
||||
"otplib": "^13.4.1",
|
||||
"pino": "^10.3.1",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-hook-form": "^7.83.0",
|
||||
"resend": "^6.18.1",
|
||||
"sanitize-html": "^2.17.6",
|
||||
"server-only": "^0.0.1",
|
||||
"sharp": "^0.35.3",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -85,27 +80,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.6",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@lhci/cli": "^0.15.1",
|
||||
"@next/bundle-analyzer": "^16.2.12",
|
||||
"@next/eslint-plugin-next": "^16.2.12",
|
||||
"@playwright/test": "1.62.0",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/node": "^26.1.2",
|
||||
"@types/nodemailer": "^8.0.1",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/sanitize-html": "^2.16.1",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"eslint": "10.8.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-security": "^4.0.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"husky": "^9.1.7",
|
||||
"knip": "^6.29.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
@@ -113,8 +99,7 @@
|
||||
"prisma": "^7.9.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tsx": "^4.23.1",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "8.1.5",
|
||||
"vitest": "4.1.10"
|
||||
}
|
||||
|
||||
Generated
+545
-988
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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,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(() => ({
|
||||
|
||||
@@ -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
@@ -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,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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user