feat(payers): gerar share_code na aplicação e remover pgcrypto

Move a geração do share_code do PostgreSQL para a camada de aplicação,
eliminando a dependência da extensão pgcrypto no setup do banco.

- schema: drop default substr(encode(gen_random_bytes(24), 'base64'), 1, 24)
  da coluna share_code em pagadores (continua NOT NULL)
- nova util generateShareCode() em shared/lib/payers/share-code.ts
  (server-only, usa crypto.randomBytes do Node)
- chamadas explícitas em createPayerAction, ensureDefaultPagadorForUser,
  resetUserAppData e mock-data ao inserir pagadores
- migration 0028_fancy_reaper renumerada (0027 já estava ocupado por
  arquivo órfão); journal e snapshot atualizados
- remove etapa de habilitação de pgcrypto do docker-entrypoint.sh
- remove scripts/postgres/ (init.sql e enable-extensions.ts)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-04-26 22:52:36 +00:00
parent 791fec7751
commit 39f3cd8b20
12 changed files with 2938 additions and 75 deletions

View File

@@ -1,15 +1,5 @@
#!/bin/sh #!/bin/sh
echo "Habilitando extensão pgcrypto..."
node -e "
const { Client } = require('/app/migrate/node_modules/pg');
const c = new Client({ connectionString: process.env.DATABASE_URL });
c.connect()
.then(() => c.query('CREATE EXTENSION IF NOT EXISTS pgcrypto'))
.then(() => c.end())
.catch((e) => { console.error('Aviso pgcrypto:', e.message); process.exit(0); });
"
echo "Rodando migrations..." echo "Rodando migrations..."
MIGRATED=0 MIGRATED=0
for i in 1 2 3 4 5; do for i in 1 2 3 4 5; do

View File

@@ -0,0 +1 @@
ALTER TABLE "pagadores" ALTER COLUMN "share_code" DROP DEFAULT;

File diff suppressed because it is too large Load Diff

View File

@@ -190,6 +190,13 @@
"when": 1777042423451, "when": 1777042423451,
"tag": "0026_bored_eternity", "tag": "0026_bored_eternity",
"breakpoints": true "breakpoints": true
},
{
"idx": 28,
"version": "7",
"when": 1777153372633,
"tag": "0028_fancy_reaper",
"breakpoints": true
} }
] ]
} }

View File

@@ -44,6 +44,7 @@ import {
PAYER_ROLE_THIRD_PARTY, PAYER_ROLE_THIRD_PARTY,
PAYER_STATUS_OPTIONS, PAYER_STATUS_OPTIONS,
} from "@/shared/lib/payers/constants"; } from "@/shared/lib/payers/constants";
import { generateShareCode } from "@/shared/lib/payers/share-code";
import { normalizeNameFromEmail } from "@/shared/lib/payers/utils"; import { normalizeNameFromEmail } from "@/shared/lib/payers/utils";
import { import {
addMonthsToDate, addMonthsToDate,
@@ -537,6 +538,7 @@ async function ensureAdminPayer(targetUser: typeof user.$inferSelect) {
note: null, note: null,
role: PAYER_ROLE_ADMIN, role: PAYER_ROLE_ADMIN,
isAutoSend: false, isAutoSend: false,
shareCode: generateShareCode(),
userId: targetUser.id, userId: targetUser.id,
}) })
.returning({ id: payers.id, name: payers.name }); .returning({ id: payers.id, name: payers.name });
@@ -870,6 +872,7 @@ async function main() {
note: definition.note, note: definition.note,
role: PAYER_ROLE_THIRD_PARTY, role: PAYER_ROLE_THIRD_PARTY,
isAutoSend: definition.isAutoSend, isAutoSend: definition.isAutoSend,
shareCode: generateShareCode(),
userId: targetUser.id, userId: targetUser.id,
}) })
.returning({ id: payers.id }); .returning({ id: payers.id });

View File

@@ -1,45 +0,0 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { config } from "dotenv";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
// Load environment variables from .env
config();
async function initDatabase() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("DATABASE_URL environment variable is required");
process.exit(1);
}
const pool = new Pool({ connectionString: databaseUrl });
const db = drizzle(pool);
try {
console.log("🔧 Initializing database extensions...");
// Read and execute init.sql as a single query
const initSqlPath = path.join(
process.cwd(),
"scripts",
"postgres",
"init.sql",
);
const initSql = fs.readFileSync(initSqlPath, "utf-8");
console.log("Executing init.sql...");
await db.execute(initSql);
console.log("✅ Database initialization completed");
} catch (error) {
console.error("❌ Database initialization failed:", error);
process.exit(1);
} finally {
await pool.end();
}
}
initDatabase();

View File

@@ -1,10 +0,0 @@
-- Script de inicialização do PostgreSQL para Docker
-- Este script é executado automaticamente quando o banco é criado pela primeira vez
-- Habilitar extensão pgcrypto (necessária para gen_random_bytes usado pelo Drizzle)
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Log de sucesso
DO $$
BEGIN
RAISE NOTICE '✅ Extensão pgcrypto habilitada com sucesso';
END $$;

View File

@@ -236,9 +236,7 @@ export const payers = pgTable(
note: text("anotacao"), note: text("anotacao"),
role: text("role"), role: text("role"),
isAutoSend: boolean("is_auto_send").notNull().default(false), isAutoSend: boolean("is_auto_send").notNull().default(false),
shareCode: text("share_code") shareCode: text("share_code").notNull(),
.notNull()
.default(sql`substr(encode(gen_random_bytes(24), 'base64'), 1, 24)`),
lastMailAt: timestamp("last_mail", { lastMailAt: timestamp("last_mail", {
mode: "date", mode: "date",
withTimezone: true, withTimezone: true,

View File

@@ -1,6 +1,5 @@
"use server"; "use server";
import { randomBytes } from "node:crypto";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { z } from "zod"; import { z } from "zod";
@@ -17,6 +16,7 @@ import {
PAYER_ROLE_THIRD_PARTY, PAYER_ROLE_THIRD_PARTY,
PAYER_STATUS_OPTIONS, PAYER_STATUS_OPTIONS,
} from "@/shared/lib/payers/constants"; } from "@/shared/lib/payers/constants";
import { generateShareCode } from "@/shared/lib/payers/share-code";
import { normalizeAvatarPath } from "@/shared/lib/payers/utils"; import { normalizeAvatarPath } from "@/shared/lib/payers/utils";
import { noteSchema, uuidSchema } from "@/shared/lib/schemas/common"; import { noteSchema, uuidSchema } from "@/shared/lib/schemas/common";
import type { ActionResult } from "@/shared/lib/types/actions"; import type { ActionResult } from "@/shared/lib/types/actions";
@@ -83,12 +83,6 @@ type ShareCodeRegenerateInput = z.infer<typeof shareCodeRegenerateSchema>;
const revalidate = (userId: string) => revalidateForEntity("payers", userId); const revalidate = (userId: string) => revalidateForEntity("payers", userId);
const generateShareCode = () => {
// base64url já retorna apenas [a-zA-Z0-9_-]
// 18 bytes = 24 caracteres em base64
return randomBytes(18).toString("base64url").slice(0, 24);
};
export async function createPayerAction( export async function createPayerAction(
input: CreateInput, input: CreateInput,
): Promise<ActionResult> { ): Promise<ActionResult> {

View File

@@ -17,6 +17,7 @@ import {
PAYER_STATUS_OPTIONS, PAYER_STATUS_OPTIONS,
} from "@/shared/lib/payers/constants"; } from "@/shared/lib/payers/constants";
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id"; import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
import { generateShareCode } from "@/shared/lib/payers/share-code";
import { normalizeNameFromEmail } from "@/shared/lib/payers/utils"; import { normalizeNameFromEmail } from "@/shared/lib/payers/utils";
import { deleteS3Object } from "@/shared/lib/storage/presign"; import { deleteS3Object } from "@/shared/lib/storage/presign";
@@ -153,6 +154,7 @@ async function resetUserAppData(
note: null, note: null,
role: PAYER_ROLE_ADMIN, role: PAYER_ROLE_ADMIN,
isAutoSend: false, isAutoSend: false,
shareCode: generateShareCode(),
userId, userId,
}); });
}); });

View File

@@ -6,6 +6,7 @@ import {
PAYER_ROLE_ADMIN, PAYER_ROLE_ADMIN,
PAYER_STATUS_OPTIONS, PAYER_STATUS_OPTIONS,
} from "./constants"; } from "./constants";
import { generateShareCode } from "./share-code";
import { normalizeNameFromEmail } from "./utils"; import { normalizeNameFromEmail } from "./utils";
const DEFAULT_STATUS = PAYER_STATUS_OPTIONS[0]; const DEFAULT_STATUS = PAYER_STATUS_OPTIONS[0];
@@ -49,6 +50,7 @@ export async function ensureDefaultPagadorForUser(user: SeedUserLike) {
avatarUrl, avatarUrl,
note: null, note: null,
isAutoSend: false, isAutoSend: false,
shareCode: generateShareCode(),
userId, userId,
}); });
} }

View File

@@ -0,0 +1,6 @@
import "server-only";
import { randomBytes } from "node:crypto";
export const generateShareCode = (): string => {
return randomBytes(18).toString("base64url").slice(0, 24);
};