Remove sharp dependency and related configurations from package.json and pnpm-lock.yaml; update Prisma configuration for database connection handling and streamline database commands in BFF application. Clean up unused migration files and adjust schema.prisma to reflect recent model changes.

This commit is contained in:
barsa 2025-12-11 14:00:54 +09:00
parent 424f257bd7
commit 0e32d4004a
15 changed files with 232 additions and 742 deletions

View File

@ -24,10 +24,10 @@
"type-check": "tsc --project tsconfig.json --noEmit",
"type-check:watch": "tsc --project tsconfig.json --noEmit --watch",
"clean": "rm -rf dist tsconfig.build.tsbuildinfo",
"db:migrate": "prisma migrate dev --schema=prisma/schema.prisma",
"db:generate": "prisma generate --schema=prisma/schema.prisma",
"db:studio": "prisma studio --schema=prisma/schema.prisma",
"db:reset": "prisma migrate reset --schema=prisma/schema.prisma",
"db:migrate": "prisma migrate dev --config prisma/prisma.config.ts",
"db:generate": "prisma generate --config prisma/prisma.config.ts",
"db:studio": "prisma studio --port 5555 --config prisma/prisma.config.ts",
"db:reset": "prisma migrate reset --config prisma/prisma.config.ts",
"db:seed": "tsx prisma/seed.ts"
},
"dependencies": {

View File

@ -1,166 +0,0 @@
-- CreateEnum
CREATE TYPE "public"."UserRole" AS ENUM ('USER', 'ADMIN');
-- CreateEnum
CREATE TYPE "public"."JobStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'RETRYING');
-- CreateEnum
CREATE TYPE "public"."AuditAction" AS ENUM ('LOGIN_SUCCESS', 'LOGIN_FAILED', 'LOGOUT', 'SIGNUP', 'PASSWORD_RESET', 'PASSWORD_CHANGE', 'ACCOUNT_LOCKED', 'ACCOUNT_UNLOCKED', 'PROFILE_UPDATE', 'MFA_ENABLED', 'MFA_DISABLED', 'API_ACCESS');
-- CreateTable
CREATE TABLE "public"."users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT,
"first_name" TEXT,
"last_name" TEXT,
"company" TEXT,
"phone" TEXT,
"role" "public"."UserRole" NOT NULL DEFAULT 'USER',
"mfa_secret" TEXT,
"email_verified" BOOLEAN NOT NULL DEFAULT false,
"failed_login_attempts" INTEGER NOT NULL DEFAULT 0,
"locked_until" TIMESTAMP(3),
"last_login_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."id_mappings" (
"user_id" TEXT NOT NULL,
"whmcs_client_id" INTEGER NOT NULL,
"sf_account_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "id_mappings_pkey" PRIMARY KEY ("user_id")
);
-- CreateTable
CREATE TABLE "public"."invoices_mirror" (
"invoice_id" INTEGER NOT NULL,
"user_id" TEXT NOT NULL,
"number" TEXT NOT NULL,
"status" TEXT NOT NULL,
"amount_cents" INTEGER NOT NULL,
"currency" CHAR(3) NOT NULL,
"due_date" DATE,
"issued_at" TIMESTAMP(3),
"paid_at" TIMESTAMP(3),
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "invoices_mirror_pkey" PRIMARY KEY ("invoice_id")
);
-- CreateTable
CREATE TABLE "public"."subscriptions_mirror" (
"service_id" INTEGER NOT NULL,
"user_id" TEXT NOT NULL,
"product_name" TEXT NOT NULL,
"domain" TEXT,
"cycle" TEXT NOT NULL,
"status" TEXT NOT NULL,
"next_due" TIMESTAMP(3),
"amount_cents" INTEGER NOT NULL,
"currency" CHAR(3) NOT NULL,
"registered_at" TIMESTAMP(3) NOT NULL,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "subscriptions_mirror_pkey" PRIMARY KEY ("service_id")
);
-- CreateTable
CREATE TABLE "public"."idempotency_keys" (
"key" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "idempotency_keys_pkey" PRIMARY KEY ("key")
);
-- CreateTable
CREATE TABLE "public"."jobs" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"data" JSONB NOT NULL,
"status" "public"."JobStatus" NOT NULL DEFAULT 'PENDING',
"attempts" INTEGER NOT NULL DEFAULT 0,
"max_retries" INTEGER NOT NULL DEFAULT 3,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"processed_at" TIMESTAMP(3),
"failed_at" TIMESTAMP(3),
"error" TEXT,
CONSTRAINT "jobs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."audit_logs" (
"id" TEXT NOT NULL,
"user_id" TEXT,
"action" "public"."AuditAction" NOT NULL,
"resource" TEXT,
"details" JSONB,
"ip_address" TEXT,
"user_agent" TEXT,
"success" BOOLEAN NOT NULL DEFAULT true,
"error" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "public"."users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "id_mappings_whmcs_client_id_key" ON "public"."id_mappings"("whmcs_client_id");
-- CreateIndex
CREATE INDEX "invoices_mirror_user_id_status_idx" ON "public"."invoices_mirror"("user_id", "status");
-- CreateIndex
CREATE INDEX "invoices_mirror_user_id_due_date_idx" ON "public"."invoices_mirror"("user_id", "due_date");
-- CreateIndex
CREATE INDEX "subscriptions_mirror_user_id_status_idx" ON "public"."subscriptions_mirror"("user_id", "status");
-- CreateIndex
CREATE INDEX "subscriptions_mirror_user_id_next_due_idx" ON "public"."subscriptions_mirror"("user_id", "next_due");
-- CreateIndex
CREATE INDEX "idempotency_keys_user_id_idx" ON "public"."idempotency_keys"("user_id");
-- CreateIndex
CREATE INDEX "idempotency_keys_created_at_idx" ON "public"."idempotency_keys"("created_at");
-- CreateIndex
CREATE INDEX "jobs_status_created_at_idx" ON "public"."jobs"("status", "created_at");
-- CreateIndex
CREATE INDEX "audit_logs_user_id_action_idx" ON "public"."audit_logs"("user_id", "action");
-- CreateIndex
CREATE INDEX "audit_logs_action_created_at_idx" ON "public"."audit_logs"("action", "created_at");
-- CreateIndex
CREATE INDEX "audit_logs_created_at_idx" ON "public"."audit_logs"("created_at");
-- AddForeignKey
ALTER TABLE "public"."id_mappings" ADD CONSTRAINT "id_mappings_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."invoices_mirror" ADD CONSTRAINT "invoices_mirror_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."subscriptions_mirror" ADD CONSTRAINT "subscriptions_mirror_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."idempotency_keys" ADD CONSTRAINT "idempotency_keys_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."audit_logs" ADD CONSTRAINT "audit_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,17 +0,0 @@
-- CreateTable
CREATE TABLE "public"."sim_usage_daily" (
"id" SERIAL NOT NULL,
"account" TEXT NOT NULL,
"date" DATE NOT NULL,
"usageMb" DOUBLE PRECISION NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "sim_usage_daily_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "sim_usage_daily_account_date_idx" ON "public"."sim_usage_daily"("account", "date");
-- CreateIndex
CREATE UNIQUE INDEX "sim_usage_daily_account_date_key" ON "public"."sim_usage_daily"("account", "date");

View File

@ -1,14 +0,0 @@
-- Safety migration to ensure cached profile columns stay removed.
-- Some environments created an empty migration folder, so deploys failed
-- because Prisma could not find migration.sql. This keeps the history intact
-- while acting as a no-op when the columns already dropped earlier.
ALTER TABLE "public"."users"
DROP COLUMN IF EXISTS "first_name",
DROP COLUMN IF EXISTS "last_name",
DROP COLUMN IF EXISTS "company",
DROP COLUMN IF EXISTS "phone";
COMMENT ON TABLE "public"."users"
IS 'Portal authentication only. Profile data fetched from WHMCS via IdMapping.';

View File

@ -0,0 +1,195 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('USER', 'ADMIN');
-- CreateEnum
CREATE TYPE "AuditAction" AS ENUM ('LOGIN_SUCCESS', 'LOGIN_FAILED', 'LOGOUT', 'SIGNUP', 'PASSWORD_RESET', 'PASSWORD_CHANGE', 'ACCOUNT_LOCKED', 'ACCOUNT_UNLOCKED', 'PROFILE_UPDATE', 'MFA_ENABLED', 'MFA_DISABLED', 'API_ACCESS', 'SYSTEM_MAINTENANCE');
-- CreateEnum
CREATE TYPE "SmsType" AS ENUM ('DOMESTIC', 'INTERNATIONAL');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT,
"role" "UserRole" NOT NULL DEFAULT 'USER',
"mfa_secret" TEXT,
"email_verified" BOOLEAN NOT NULL DEFAULT false,
"failed_login_attempts" INTEGER NOT NULL DEFAULT 0,
"locked_until" TIMESTAMP(3),
"last_login_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "id_mappings" (
"user_id" TEXT NOT NULL,
"whmcs_client_id" INTEGER NOT NULL,
"sf_account_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "id_mappings_pkey" PRIMARY KEY ("user_id")
);
-- CreateTable
CREATE TABLE "audit_logs" (
"id" TEXT NOT NULL,
"user_id" TEXT,
"action" "AuditAction" NOT NULL,
"resource" TEXT,
"details" JSONB,
"ip_address" TEXT,
"user_agent" TEXT,
"success" BOOLEAN NOT NULL DEFAULT true,
"error" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sim_usage_daily" (
"id" SERIAL NOT NULL,
"account" TEXT NOT NULL,
"date" DATE NOT NULL,
"usageMb" DOUBLE PRECISION NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "sim_usage_daily_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sim_voice_options" (
"account" TEXT NOT NULL,
"voice_mail_enabled" BOOLEAN NOT NULL DEFAULT false,
"call_waiting_enabled" BOOLEAN NOT NULL DEFAULT false,
"international_roaming_enabled" BOOLEAN NOT NULL DEFAULT false,
"network_type" TEXT NOT NULL DEFAULT '4G',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "sim_voice_options_pkey" PRIMARY KEY ("account")
);
-- CreateTable
CREATE TABLE "sim_call_history_domestic" (
"id" TEXT NOT NULL,
"account" TEXT NOT NULL,
"call_date" DATE NOT NULL,
"call_time" TEXT NOT NULL,
"called_to" TEXT NOT NULL,
"location" TEXT,
"duration_sec" INTEGER NOT NULL,
"charge_yen" INTEGER NOT NULL,
"month" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "sim_call_history_domestic_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sim_call_history_international" (
"id" TEXT NOT NULL,
"account" TEXT NOT NULL,
"call_date" DATE NOT NULL,
"start_time" TEXT NOT NULL,
"stop_time" TEXT,
"country" TEXT,
"called_to" TEXT NOT NULL,
"duration_sec" INTEGER NOT NULL,
"charge_yen" INTEGER NOT NULL,
"month" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "sim_call_history_international_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sim_sms_history" (
"id" TEXT NOT NULL,
"account" TEXT NOT NULL,
"sms_date" DATE NOT NULL,
"sms_time" TEXT NOT NULL,
"sent_to" TEXT NOT NULL,
"sms_type" "SmsType" NOT NULL DEFAULT 'DOMESTIC',
"month" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "sim_sms_history_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sim_history_imports" (
"id" TEXT NOT NULL,
"month" TEXT NOT NULL,
"talk_file" TEXT,
"sms_file" TEXT,
"talk_records" INTEGER NOT NULL DEFAULT 0,
"sms_records" INTEGER NOT NULL DEFAULT 0,
"imported_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"status" TEXT NOT NULL DEFAULT 'completed',
CONSTRAINT "sim_history_imports_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "id_mappings_whmcs_client_id_key" ON "id_mappings"("whmcs_client_id");
-- CreateIndex
CREATE INDEX "audit_logs_user_id_action_idx" ON "audit_logs"("user_id", "action");
-- CreateIndex
CREATE INDEX "audit_logs_action_created_at_idx" ON "audit_logs"("action", "created_at");
-- CreateIndex
CREATE INDEX "audit_logs_created_at_idx" ON "audit_logs"("created_at");
-- CreateIndex
CREATE INDEX "sim_usage_daily_account_date_idx" ON "sim_usage_daily"("account", "date");
-- CreateIndex
CREATE UNIQUE INDEX "sim_usage_daily_account_date_key" ON "sim_usage_daily"("account", "date");
-- CreateIndex
CREATE INDEX "sim_call_history_domestic_account_month_idx" ON "sim_call_history_domestic"("account", "month");
-- CreateIndex
CREATE INDEX "sim_call_history_domestic_account_call_date_idx" ON "sim_call_history_domestic"("account", "call_date");
-- CreateIndex
CREATE UNIQUE INDEX "sim_call_history_domestic_account_call_date_call_time_calle_key" ON "sim_call_history_domestic"("account", "call_date", "call_time", "called_to");
-- CreateIndex
CREATE INDEX "sim_call_history_international_account_month_idx" ON "sim_call_history_international"("account", "month");
-- CreateIndex
CREATE INDEX "sim_call_history_international_account_call_date_idx" ON "sim_call_history_international"("account", "call_date");
-- CreateIndex
CREATE UNIQUE INDEX "sim_call_history_international_account_call_date_start_time_key" ON "sim_call_history_international"("account", "call_date", "start_time", "called_to");
-- CreateIndex
CREATE INDEX "sim_sms_history_account_month_idx" ON "sim_sms_history"("account", "month");
-- CreateIndex
CREATE INDEX "sim_sms_history_account_sms_date_idx" ON "sim_sms_history"("account", "sms_date");
-- CreateIndex
CREATE UNIQUE INDEX "sim_sms_history_account_sms_date_sms_time_sent_to_key" ON "sim_sms_history"("account", "sms_date", "sms_time", "sent_to");
-- CreateIndex
CREATE UNIQUE INDEX "sim_history_imports_month_key" ON "sim_history_imports"("month");
-- AddForeignKey
ALTER TABLE "id_mappings" ADD CONSTRAINT "id_mappings_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,9 +0,0 @@
-- Force password reset for all users due to migration from bcrypt to argon2
-- bcrypt hashes are incompatible with argon2, so all users must reset their passwords
-- Set all password hashes to NULL, which will require users to go through password reset flow
UPDATE "User" SET password_hash = NULL WHERE password_hash IS NOT NULL;
-- Log the migration for audit purposes (optional - create entry in audit_log if table exists)
-- This is a data migration, not a schema change

View File

@ -1,29 +1,22 @@
import { defineConfig } from "prisma";
import { PrismaPg } from "@prisma/adapter-pg";
import { Pool } from "pg";
import { defineConfig } from "prisma/config";
// Default connection for local development (matches docker/dev/docker-compose.yml)
const DEFAULT_DATABASE_URL = "postgresql://dev:dev@localhost:5432/portal_dev";
/**
* Prisma 7 Configuration
*
* This configuration file is required for Prisma 7+ where the datasource URL
* is no longer specified in schema.prisma. Instead, connection configuration
* is provided here for migrations and in the PrismaClient constructor for runtime.
* Centralized configuration for Prisma CLI commands (migrate, studio, etc.)
* Runtime adapter is configured in PrismaService, not here.
*
* @see https://pris.ly/d/config-datasource
* @see https://pris.ly/d/prisma7-client-config
* @see https://www.prisma.io/docs/orm/reference/prisma-config-reference
* @see https://www.prisma.io/docs/orm/prisma-schema/overview/data-sources
*/
export default defineConfig({
earlyAccess: true,
schema: "./schema.prisma",
migrate: {
adapter: async () => {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL environment variable is required for migrations");
}
const pool = new Pool({ connectionString });
return new PrismaPg(pool);
},
// Database connection for CLI commands (migrate, studio, etc.)
datasource: {
url: process.env.DATABASE_URL || DEFAULT_DATABASE_URL,
},
});

View File

@ -15,8 +15,8 @@ generator client {
datasource db {
provider = "postgresql"
// Note: Prisma 7+ requires connection URL to be passed via adapter in PrismaClient constructor
// See prisma.config.ts for migration configuration
// Prisma 7+: URL is configured via prisma.config.ts for migrations
// and via --url flag for studio. Runtime uses the adapter in PrismaClient.
}
model User {
@ -36,9 +36,6 @@ model User {
updatedAt DateTime @updatedAt @map("updated_at")
auditLogs AuditLog[]
idMapping IdMapping?
idempotencyKeys IdempotencyKey[]
invoicesMirror InvoiceMirror[]
subscriptionsMirror SubscriptionMirror[]
@@map("users")
}
@ -54,71 +51,6 @@ model IdMapping {
@@map("id_mappings")
}
model InvoiceMirror {
invoiceId Int @id @map("invoice_id")
userId String @map("user_id")
number String
status String
amountCents Int @map("amount_cents")
currency String @db.Char(3)
dueDate DateTime? @map("due_date") @db.Date
issuedAt DateTime? @map("issued_at")
paidAt DateTime? @map("paid_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, status])
@@index([userId, dueDate])
@@map("invoices_mirror")
}
model SubscriptionMirror {
serviceId Int @id @map("service_id")
userId String @map("user_id")
productName String @map("product_name")
domain String?
cycle String
status String
nextDue DateTime? @map("next_due")
amountCents Int @map("amount_cents")
currency String @db.Char(3)
registeredAt DateTime @map("registered_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, status])
@@index([userId, nextDue])
@@map("subscriptions_mirror")
}
model IdempotencyKey {
key String @id
userId String @map("user_id")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([createdAt])
@@map("idempotency_keys")
}
model Job {
id String @id @default(uuid())
name String
data Json
status JobStatus @default(PENDING)
attempts Int @default(0)
maxRetries Int @default(3) @map("max_retries")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
processedAt DateTime? @map("processed_at")
failedAt DateTime? @map("failed_at")
error String?
@@index([status, createdAt])
@@map("jobs")
}
model AuditLog {
id String @id @default(uuid())
userId String? @map("user_id")
@ -143,14 +75,6 @@ enum UserRole {
ADMIN
}
enum JobStatus {
PENDING
PROCESSING
COMPLETED
FAILED
RETRYING
}
enum AuditAction {
LOGIN_SUCCESS
LOGIN_FAILED

View File

@ -1,10 +1,10 @@
import type { INestApplication } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import type { NestExpressApplication } from "@nestjs/platform-express";
import { Logger } from "nestjs-pino";
import helmet from "helmet";
import cookieParser from "cookie-parser";
import * as express from "express";
import type { CookieOptions, Response, NextFunction, Request } from "express";
/* eslint-disable @typescript-eslint/no-namespace */
@ -22,12 +22,15 @@ import { UnifiedExceptionFilter } from "../core/http/exception.filter.js";
import { AppModule } from "../app.module.js";
export async function bootstrap(): Promise<INestApplication> {
const app = await NestFactory.create(AppModule, {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
bufferLogs: true,
// bodyParser is enabled by default in NestJS
rawBody: true, // Enable raw body access for debugging
});
// Configure body parser limits via Express adapter
app.useBodyParser("json", { limit: "10mb" });
app.useBodyParser("urlencoded", { extended: true, limit: "10mb" });
// Set Pino as the logger
app.useLogger(app.get(Logger));
@ -63,10 +66,6 @@ export async function bootstrap(): Promise<INestApplication> {
expressInstance.disable("x-powered-by");
}
// Configure JSON body parser with proper limits
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// Enhanced cookie parser with security options
app.use(cookieParser());

View File

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -1,122 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720">
<defs>
<style>
.lane { fill: #f7f9fc; stroke: #d0d7de; }
.lane-header { fill: #e6eef8; stroke: #b6c6d6; font: 600 16px sans-serif; }
.step { fill: #ffffff; stroke: #94a3b8; rx: 8; }
.step-text { font: 12px sans-serif; fill: #0f172a; }
.label { font: 12px sans-serif; fill: #334155; }
.arrow { stroke: #475569; stroke-width: 2; fill: none; marker-end: url(#arrowhead); }
.num { fill: #2563eb; font: 600 12px sans-serif; }
</style>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#475569" />
</marker>
</defs>
<!-- Lanes -->
<rect x="30" y="20" width="260" height="680" class="lane"/>
<rect x="330" y="20" width="260" height="680" class="lane"/>
<rect x="630" y="20" width="260" height="680" class="lane"/>
<rect x="930" y="20" width="260" height="680" class="lane"/>
<!-- Headers -->
<rect x="30" y="20" width="260" height="40" class="lane-header"/>
<rect x="330" y="20" width="260" height="40" class="lane-header"/>
<rect x="630" y="20" width="260" height="40" class="lane-header"/>
<rect x="930" y="20" width="260" height="40" class="lane-header"/>
<text x="160" y="46" text-anchor="middle" class="lane-header">Customer</text>
<text x="460" y="46" text-anchor="middle" class="lane-header">Portal (BFF)</text>
<text x="760" y="46" text-anchor="middle" class="lane-header">Salesforce</text>
<text x="1060" y="46" text-anchor="middle" class="lane-header">WHMCS</text>
<!-- Steps: Customer -->
<rect x="50" y="90" width="220" height="56" class="step"/>
<text x="60" y="110" class="step-text">1) Sign Up &amp; Link Account</text>
<text x="60" y="126" class="step-text">Provide Customer Number</text>
<rect x="50" y="170" width="220" height="56" class="step"/>
<text x="60" y="190" class="step-text">2) Add Payment Method</text>
<text x="60" y="206" class="step-text">Secure SSO to WHMCS</text>
<rect x="50" y="250" width="220" height="56" class="step"/>
<text x="60" y="270" class="step-text">3) Browse Catalog</text>
<text x="60" y="286" class="step-text">Select Plan &amp; Addons</text>
<rect x="50" y="510" width="220" height="56" class="step"/>
<text x="60" y="530" class="step-text">7) View Invoices</text>
<text x="60" y="546" class="step-text">Pay via SSO</text>
<!-- Steps: Portal -->
<rect x="350" y="120" width="220" height="56" class="step"/>
<text x="360" y="140" class="step-text">Validate mapping &amp; address</text>
<text x="360" y="156" class="step-text">Create WHMCS client</text>
<rect x="350" y="200" width="220" height="56" class="step"/>
<text x="360" y="220" class="step-text">Open WHMCS payment page</text>
<text x="360" y="236" class="step-text">(SSO)</text>
<rect x="350" y="300" width="220" height="56" class="step"/>
<text x="360" y="320" class="step-text">Create Order in SF</text>
<text x="360" y="336" class="step-text">(snapshot address)</text>
<rect x="350" y="380" width="220" height="56" class="step"/>
<text x="360" y="400" class="step-text">Provision in WHMCS</text>
<text x="360" y="416" class="step-text">after approval</text>
<rect x="350" y="510" width="220" height="56" class="step"/>
<text x="360" y="530" class="step-text">Show subs &amp; invoices</text>
<!-- Steps: Salesforce -->
<rect x="650" y="300" width="220" height="56" class="step"/>
<text x="660" y="320" class="step-text">Review &amp; Approve</text>
<text x="660" y="336" class="step-text">Order</text>
<!-- Steps: WHMCS -->
<rect x="950" y="170" width="220" height="56" class="step"/>
<text x="960" y="190" class="step-text">Store Payment Methods</text>
<rect x="950" y="380" width="220" height="56" class="step"/>
<text x="960" y="400" class="step-text">Create Services &amp; Invoice</text>
<!-- Arrows + labels -->
<!-- Signup -> Portal create WHMCS client -->
<path d="M270 118 C 300 118, 320 118, 350 118" class="arrow"/>
<text x="310" y="110" class="label" text-anchor="middle">Customer Number</text>
<!-- Add payment -> Portal -> WHMCS -->
<path d="M270 198 C 300 198, 320 198, 350 198" class="arrow"/>
<path d="M570 228 C 600 228, 900 198, 950 198" class="arrow"/>
<text x="760" y="188" class="label" text-anchor="middle">SSO Payment Page</text>
<!-- Browse -> Create Order (Portal) -->
<path d="M270 278 C 300 278, 320 318, 350 318" class="arrow"/>
<text x="310" y="300" class="label" text-anchor="middle">Selected items</text>
<!-- Portal -> SF (create order) -->
<path d="M570 328 C 600 328, 620 328, 650 328" class="arrow"/>
<text x="610" y="320" class="label" text-anchor="middle">Order + address</text>
<!-- SF approve -> Portal provision -->
<path d="M760 356 C 760 390, 570 408, 570 408" class="arrow"/>
<text x="680" y="388" class="label">Approved</text>
<!-- Portal provision -> WHMCS -->
<path d="M570 408 C 770 408, 900 408, 950 408" class="arrow"/>
<text x="760" y="398" class="label" text-anchor="middle">AddOrder + Accept</text>
<!-- WHMCS -> Portal (subs & invoices) -->
<path d="M950 538 C 900 538, 600 538, 570 538" class="arrow"/>
<text x="760" y="528" class="label" text-anchor="middle">Subscriptions + Invoices</text>
<!-- Portal -> Customer (display) -->
<path d="M350 538 C 320 538, 300 538, 270 538" class="arrow"/>
<!-- Portal -> WHMCS (pay SSO) -->
<path d="M350 566 C 600 566, 900 566, 950 566" class="arrow"/>
<text x="760" y="560" class="label" text-anchor="middle">Pay Invoice (SSO)</text>
<!-- Legend -->
<text x="30" y="700" class="label">Legend: SSO = secure single sign-on; SF = Salesforce</text>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -150,15 +150,6 @@ Use these slide-ready bullets to explain, in plain language, how our portal work
Tip: Pair these slides with a simple swimlane diagram (Customer, Portal, Salesforce, WHMCS) showing the handoffs at order and activation.
References for deeper reading (optional for presenters)
- Address flow details: docs/ADDRESS_SYSTEM.md
- Technical overview: docs/PORTAL-INTEGRATION-OVERVIEW.md
Diagram (PNG/SVG for slides)
- Swimlane visual: docs/assets/portal-swimlane.svg
---
## 11) Migration (Moving Existing WHMCS Users Into the Portal)

View File

@ -58,7 +58,6 @@
"globals": "^16.5.0",
"husky": "^9.1.7",
"prettier": "^3.7.4",
"sharp": "^0.33.5",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.49.0"

242
pnpm-lock.yaml generated
View File

@ -43,9 +43,6 @@ importers:
prettier:
specifier: ^3.7.4
version: 3.7.4
sharp:
specifier: ^0.33.5
version: 0.33.5
tsx:
specifier: ^4.21.0
version: 4.21.0
@ -775,65 +772,33 @@ packages:
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.33.5':
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.0.4':
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.0.4':
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.0.4':
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
@ -849,64 +814,32 @@ packages:
cpu: [riscv64]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@ -925,59 +858,30 @@ packages:
cpu: [riscv64]
os: [linux]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@ -989,24 +893,12 @@ packages:
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.33.5':
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.33.5':
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@ -2836,13 +2728,6 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
@ -3828,9 +3713,6 @@ packages:
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
is-arrayish@0.3.4:
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
is-async-function@2.1.1:
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
engines: {node: '>= 0.4'}
@ -5265,10 +5147,6 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
sharp@0.33.5:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@ -5304,9 +5182,6 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-swizzle@0.2.4:
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
sirv@2.0.4:
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
engines: {node: '>= 10'}
@ -6449,47 +6324,25 @@ snapshots:
'@img/colour@1.0.0':
optional: true
'@img/sharp-darwin-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4
optional: true
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.0.4
optional: true
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.0.4':
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.0.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.0.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.0.5':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
@ -6499,45 +6352,23 @@ snapshots:
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.0.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.0.4':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.0.4
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.0.5
optional: true
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
@ -6553,51 +6384,26 @@ snapshots:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
'@img/sharp-linux-s390x@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.0.4
optional: true
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
'@img/sharp-linux-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.0.4
optional: true
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.33.5':
dependencies:
'@emnapi/runtime': 1.7.1
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.7.1
@ -6606,15 +6412,9 @@ snapshots:
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-win32-ia32@0.33.5':
optional: true
'@img/sharp-win32-ia32@0.34.5':
optional: true
'@img/sharp-win32-x64@0.33.5':
optional: true
'@img/sharp-win32-x64@0.34.5':
optional: true
@ -8687,16 +8487,6 @@ snapshots:
color-name@1.1.4: {}
color-string@1.9.1:
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.4
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
colorette@2.0.20: {}
combined-stream@1.0.8:
@ -9891,8 +9681,6 @@ snapshots:
is-arrayish@0.2.1: {}
is-arrayish@0.3.4: {}
is-async-function@2.1.1:
dependencies:
async-function: 1.0.0
@ -11531,32 +11319,6 @@ snapshots:
setprototypeof@1.2.0: {}
sharp@0.33.5:
dependencies:
color: 4.2.3
detect-libc: 2.1.2
semver: 7.7.3
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5
'@img/sharp-libvips-darwin-arm64': 1.0.4
'@img/sharp-libvips-darwin-x64': 1.0.4
'@img/sharp-libvips-linux-arm': 1.0.5
'@img/sharp-libvips-linux-arm64': 1.0.4
'@img/sharp-libvips-linux-s390x': 1.0.4
'@img/sharp-libvips-linux-x64': 1.0.4
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
'@img/sharp-linux-arm': 0.33.5
'@img/sharp-linux-arm64': 0.33.5
'@img/sharp-linux-s390x': 0.33.5
'@img/sharp-linux-x64': 0.33.5
'@img/sharp-linuxmusl-arm64': 0.33.5
'@img/sharp-linuxmusl-x64': 0.33.5
'@img/sharp-wasm32': 0.33.5
'@img/sharp-win32-ia32': 0.33.5
'@img/sharp-win32-x64': 0.33.5
sharp@0.34.5:
dependencies:
'@img/colour': 1.0.0
@ -11627,10 +11389,6 @@ snapshots:
signal-exit@4.1.0: {}
simple-swizzle@0.2.4:
dependencies:
is-arrayish: 0.3.4
sirv@2.0.4:
dependencies:
'@polka/url': 1.0.0-next.29

View File

@ -1,41 +0,0 @@
#!/usr/bin/env node
/**
* Simple SVG -> PNG converter using sharp.
* Usage: node svg2png.mjs <input.svg> <output.png> <width> <height>
*/
import fs from "node:fs/promises";
import path from "node:path";
import sharp from "sharp";
async function main() {
const [, , inPath, outPath, wArg, hArg] = process.argv;
if (!inPath || !outPath || !wArg || !hArg) {
console.error("Usage: node svg2png.mjs <input.svg> <output.png> <width> <height>");
process.exit(1);
}
const width = Number(wArg);
const height = Number(hArg);
if (!width || !height) {
console.error("Width and height must be numbers");
process.exit(1);
}
const absIn = path.resolve(inPath);
const absOut = path.resolve(outPath);
const svg = await fs.readFile(absIn);
// Render with background white to avoid transparency issues in slides
const png = await sharp(svg, { density: 300 })
.resize(width, height, { fit: "contain", background: { r: 255, g: 255, b: 255, alpha: 1 } })
.png({ compressionLevel: 9 })
.toBuffer();
await fs.mkdir(path.dirname(absOut), { recursive: true });
await fs.writeFile(absOut, png);
console.log(`Wrote ${absOut} (${width}x${height})`);
}
main().catch(err => {
console.error("svg2png failed:", err?.message || err);
process.exit(1);
});