9 Commits

Author SHA1 Message Date
Felipe Coutinho
19b5aa00ee docs(changelog): registrar vetorização dos logos em v2.4.4
Adiciona ao bloco da v2.4.4 as mudanças de logo (split em LogoIcon/
LogoText, SVGs inline, troca dos PNGs por SVGs no public/ e
rasterização em alta resolução nos PDFs) e o fix do baseUrl no
tsconfig. Também atualiza a data da release para 2026-04-27.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 00:13:54 +00:00
Felipe Coutinho
863ccc0fd2 refactor(exports): renderizar logos SVG em alta resolução no PDF
Atualiza loadExportLogoDataUrl para carregar SVGs e rasterizar no canvas
a 4× a resolução natural antes de retornar o data URL — preserva nitidez
quando o PDF amplia a imagem. Default do path mudou para
/images/logo_text.svg.

Os exports de categorias e lançamentos agora apontam para os arquivos
.svg em vez dos .png removidos.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 00:11:21 +00:00
Felipe Coutinho
29d99cbedb chore(assets): trocar PNGs do logo por SVGs vetorizados
- adiciona public/images/logo_small.svg e logo_text.svg com width/height
  explícitos (necessário para naturalWidth/Height funcionar via <img>)
- remove os PNGs antigos (logo_small.png e logo_text.png)
- atualiza referência no README.md (header) para logo_small.svg

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 00:11:15 +00:00
Felipe Coutinho
dbeb98bbe4 refactor(logo): vetorizar e separar LogoIcon/LogoText em arquivos próprios
Substitui as PNGs raster do componente Logo por SVGs inline e quebra
em dois subcomponentes reutilizáveis:

- LogoIcon (src/shared/components/logo-icon.tsx): SVG do ícone laranja
  (viewBox 0 0 200 200), aceita SVGProps via spread
- LogoText (src/shared/components/logo-text.tsx): SVG do wordmark
  (viewBox 0 0 574.201 89.6), fill #000 + dark:invert para alternar
  preto/branco conforme o tema
- Logo (orquestrador): mantém a API atual (variants full/compact/small,
  invertTextOnDark, colorIcon, iconClassName, textClassName) e agora
  renderiza os SVGs em vez de next/image

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 00:11:10 +00:00
Felipe Coutinho
c0436dc2ac fix(tsconfig): remover baseUrl para evitar erro de deprecação no TS 7
A remoção de "ignoreDeprecations": "6.0" no commit anterior reabriu o
erro TS5101 sobre baseUrl. Como moduleResolution: bundler resolve os
paths relativos ao próprio tsconfig.json, baseUrl é redundante e pode
ser removido.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 22:58:09 +00:00
Felipe Coutinho
e1e76fadc0 chore(release): v2.4.4
Versão dedicada a remover a dependência de pgcrypto e a enxugar os
backups. CHANGELOG, badge do README e fluxo de restore atualizados;
script pnpm db:extensions removido do package.json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 22:52:54 +00:00
Felipe Coutinho
9b2c15ef7d chore: ajustes de configuração diversos
- tsconfig: target ES2017 → ES2022, remove ignoreDeprecations 6.0
- gitignore: ignora pasta .codex
- next.config: remove linha em branco supérflua

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 22:52:50 +00:00
Felipe Coutinho
fbe3fceb9f chore(backup): escopar dumps aos schemas public e drizzle
Adiciona --schema=public --schema=drizzle aos pg_dump (modos remote e
docker), descartando os schemas internos do Supabase (auth, realtime,
storage, vault, graphql, etc.). Restaurações em PostgreSQL padrão
deixam de produzir os ~148 erros de role/extension does not exist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 22:52:42 +00:00
Felipe Coutinho
39f3cd8b20 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>
2026-04-26 22:52:36 +00:00
29 changed files with 3080 additions and 172 deletions

1
.gitignore vendored
View File

@@ -106,6 +106,7 @@ docker-compose.override.yml
.cursor/ .cursor/
QWEN.md QWEN.md
AGENTS.md AGENTS.md
.codex
# === Backups locais === # === Backups locais ===
/backup/ /backup/

View File

@@ -7,6 +7,39 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
## [Unreleased] ## [Unreleased]
## [2.4.4] - 2026-04-27
Esta versão remove a dependência da extensão `pgcrypto` do PostgreSQL para a geração do `share_code` em pagadores. O default a nível de banco (`gen_random_bytes`) foi removido — agora a aplicação gera o código sempre via `crypto.randomBytes` do Node.js, num utilitário compartilhado. A consequência prática é que o setup inicial fica mais simples: não há mais script de habilitação de extensão, nem etapa extra no primeiro `db:push`, e bancos restaurados de dumps externos não precisam ter `pgcrypto` instalada. O script de backup também foi enxugado para gerar dumps focados nos schemas relevantes (`public` e `drizzle`), descartando os schemas internos do Supabase e eliminando os ~148 erros de restore em PostgreSQL padrão. Por fim, os logos da marca (ícone laranja e wordmark) foram vetorizados: as PNGs antigas foram substituídas por SVGs inline em componentes próprios e por arquivos `.svg` no `public/`, escalando perfeitamente em qualquer tamanho — inclusive nos PDFs exportados, que agora rasterizam o SVG em alta resolução.
### Alterado
- Schema: coluna `share_code` em `pagadores` perdeu o default `substr(encode(gen_random_bytes(24), 'base64'), 1, 24)` — campo continua `NOT NULL` e a aplicação passa a fornecer o valor explicitamente em todas as inserções
- Pagadores: nova função utilitária `generateShareCode()` em `src/shared/lib/payers/share-code.ts` (server-only) — usa `crypto.randomBytes(18).toString("base64url").slice(0, 24)`
- Pagadores: `createPayerAction`, `ensureDefaultPagadorForUser`, `resetUserAppData` (settings) e `mock-data.ts` agora chamam `generateShareCode()` ao inserir um pagador
- Backup: `scripts/backup.sh` agora dumpa apenas os schemas `public` e `drizzle` — schemas internos do Supabase (`auth`, `realtime`, `storage`, `vault`, `graphql`, `graphql_public`, `extensions`, `pgbouncer`) e suas extensions/roles deixam de poluir os dumps. Restaurações em PostgreSQL padrão passam a executar sem os ~148 erros de `role/extension does not exist`
- Logo: `Logo` foi quebrado em três arquivos — `src/shared/components/logo.tsx` (orquestrador), `logo-icon.tsx` (ícone laranja em SVG inline, viewBox `0 0 200 200`) e `logo-text.tsx` (wordmark em SVG inline, viewBox `0 0 574.201 89.6`). API pública (`variant`, `invertTextOnDark`, `colorIcon`, `iconClassName`, `textClassName`) preservada
- Assets: `public/images/logo_small.png` e `logo_text.png` substituídos por `logo_small.svg` e `logo_text.svg` (com `width`/`height` explícitos para compatibilidade com `<img>` em canvas)
- Exports: `loadExportLogoDataUrl` agora carrega SVG e rasteriza no canvas a 4× a resolução natural antes de gerar o data URL — mantém nitidez quando o PDF amplia a imagem
### Removido
- Pasta `scripts/postgres/` (continha `init.sql` e `enable-extensions.ts`)
- Script `pnpm db:extensions` no `package.json`
- Referências ao `pnpm db:extensions` no README
- `public/images/logo_small.png` e `public/images/logo_text.png` (substituídos pelos `.svg`)
### Corrigido
- Migrations: conflito de numeração resolvido — `0027_fancy_reaper` renomeado para `0028_fancy_reaper` (o número 0027 já estava ocupado pelo arquivo órfão `0027_glorious_mindworm`); journal e snapshot atualizados
- TS: removido `baseUrl` do `tsconfig.json` para evitar erro `TS5101` (deprecação no TS 7) — `moduleResolution: bundler` resolve os `paths` relativos ao próprio `tsconfig`, dispensando `baseUrl`
### Documentação
- README: seção Backup atualizada — arquivos gerados agora especificam que apenas os schemas `public` e `drizzle` são dumpados
- README: seção Restore reescrita com o fluxo correto para banco Docker (`DROP SCHEMA public CASCADE` + `pg_restore --clean --if-exists --disable-triggers`)
- README: comando rápido de Docker Compose de backup/restore substituído por `pnpm backup`
- README: header passa a apontar para `logo_small.svg`
## [2.4.3] - 2026-04-25 ## [2.4.3] - 2026-04-25
Esta versão amplia o trabalho com lançamentos divididos: anexos passam a ser visíveis para pessoas com acesso compartilhado, a importação para conta própria copia os arquivos de forma independente e a edição ganha a opção de aplicar a alteração nos dois lados do par. Três caminhos de deleção foram corrigidos para não deixar arquivos órfãos no storage. Também traz refresh visual nos badges de tipo e radio buttons, prefetch server-side de logos para reduzir chamadas de API no dashboard, e ajustes pontuais no healthcheck do container e em rótulos da UI. Esta versão amplia o trabalho com lançamentos divididos: anexos passam a ser visíveis para pessoas com acesso compartilhado, a importação para conta própria copia os arquivos de forma independente e a edição ganha a opção de aplicar a alteração nos dois lados do par. Três caminhos de deleção foram corrigidos para não deixar arquivos órfãos no storage. Também traz refresh visual nos badges de tipo e radio buttons, prefetch server-side de logos para reduzir chamadas de API no dashboard, e ajustes pontuais no healthcheck do container e em rótulos da UI.

View File

@@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="./public/images/logo_small.png" alt="OpenMonetis Logo" height="80" /> <img src="./public/images/logo_small.svg" alt="OpenMonetis Logo" height="80" />
</p> </p>
<p align="center"> <p align="center">
@@ -8,7 +8,7 @@
> **⚠️ Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor. > **⚠️ Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor.
[![Version](https://img.shields.io/badge/version-2.4.3-blue?style=flat-square)](CHANGELOG.md) [![Version](https://img.shields.io/badge/version-2.4.4-blue?style=flat-square)](CHANGELOG.md)
[![Next.js](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js)](https://nextjs.org/) [![Next.js](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-blue?style=flat-square&logo=postgresql)](https://www.postgresql.org/) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-blue?style=flat-square&logo=postgresql)](https://www.postgresql.org/)
@@ -196,13 +196,10 @@ cp .env.example .env
# 4. Suba o banco # 4. Suba o banco
pnpm docker:db pnpm docker:db
# 5. Habilite extensões do PostgreSQL (apenas no primeiro setup) # 5. Aplique o schema no banco (apenas no primeiro setup)
pnpm db:extensions
# 6. Aplique o schema no banco (apenas no primeiro setup)
pnpm db:push pnpm db:push
# 7. Inicie o app com hot-reload # 6. Inicie o app com hot-reload
pnpm dev pnpm dev
``` ```
@@ -240,7 +237,6 @@ pnpm lint:fix # Biome auto-fix
pnpm db:generate # Gerar migrations pnpm db:generate # Gerar migrations
pnpm db:migrate # Executar migrations pnpm db:migrate # Executar migrations
pnpm db:push # Push schema direto (dev) pnpm db:push # Push schema direto (dev)
pnpm db:extensions # Habilitar extensões PostgreSQL (rodar uma vez)
pnpm db:studio # Drizzle Studio (UI visual) pnpm db:studio # Drizzle Studio (UI visual)
``` ```
@@ -291,8 +287,7 @@ docker compose up -d app
docker compose exec app sh # Shell da aplicação docker compose exec app sh # Shell da aplicação
docker compose exec db psql -U openmonetis -d openmonetis_db # Shell do banco docker compose exec db psql -U openmonetis -d openmonetis_db # Shell do banco
docker compose ps # Status docker compose ps # Status
docker compose exec db pg_dump -U openmonetis openmonetis_db > backup.sql # Backup pnpm backup # Backup (ver seção Backup)
docker compose exec -T db psql -U openmonetis -d openmonetis_db < backup.sql # Restore
``` ```
### Customizando portas ### Customizando portas
@@ -318,9 +313,9 @@ Cada execução gera **3 arquivos** em `backup/`:
| Arquivo | Conteúdo | Uso | | Arquivo | Conteúdo | Uso |
|---|---|---| |---|---|---|
| `openmonetis_YYYY-MM-DD_HH-MM.dump` | Dump custom do PostgreSQL (binário) | Restore completo via `pg_restore` | | `openmonetis_YYYY-MM-DD_HH-MM.dump` | Dump custom dos schemas `public` + `drizzle` | Restore completo via `pg_restore` |
| `openmonetis_YYYY-MM-DD_HH-MM.sql.gz` | Dump SQL completo compactado | Inspeção manual, portabilidade | | `openmonetis_YYYY-MM-DD_HH-MM.sql.gz` | Dump SQL compactado dos schemas `public` + `drizzle` | Inspeção manual, portabilidade |
| `openmonetis_YYYY-MM-DD_HH-MM.data.sql.gz` | Apenas os dados da schema `public` | Migração parcial, seed de outro ambiente | | `openmonetis_YYYY-MM-DD_HH-MM.data.sql.gz` | Apenas os dados do schema `public` (sem DDL) | Migração parcial, seed de outro ambiente |
### Modos de conexão ### Modos de conexão
@@ -354,16 +349,19 @@ crontab -e
### Restore ### Restore
```bash ```bash
# A partir do .dump (recomendado — mais rápido) # 1. Zerar o banco
pg_restore --clean --no-owner --no-privileges \ docker exec <container-db> psql -U openmonetis -d openmonetis_db \
-d "postgresql://user:senha@host:5432/openmonetis_db" \ -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
backup/openmonetis_YYYY-MM-DD_HH-MM.dump
# A partir do .sql.gz (banco local via Docker) # 2. Restaurar schema + dados (um comando)
gunzip -c backup/openmonetis_YYYY-MM-DD_HH-MM.sql.gz | \ docker exec -i <container-db> pg_restore \
docker compose exec -T db psql -U openmonetis -d openmonetis_db -U openmonetis -d openmonetis_db \
--clean --if-exists --disable-triggers --no-owner --no-privileges \
< backup/openmonetis_YYYY-MM-DD_HH-MM.dump
``` ```
> `--disable-triggers` é necessário para evitar erros de FK durante o restore (os dados são inseridos fora de ordem). O usuário `openmonetis` tem permissão para isso.
--- ---
## ☁️ Storage S3 Compatível ## ☁️ Storage S3 Compatível

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

@@ -8,7 +8,6 @@ const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
cacheComponents: true, cacheComponents: true,
reactCompiler: true, reactCompiler: true,
images: { images: {
remotePatterns: [ remotePatterns: [
new URL("https://lh3.googleusercontent.com/**"), new URL("https://lh3.googleusercontent.com/**"),

View File

@@ -1,6 +1,6 @@
{ {
"name": "openmonetis", "name": "openmonetis",
"version": "2.4.3", "version": "2.4.4",
"private": true, "private": true,
"packageManager": "pnpm@10.33.0", "packageManager": "pnpm@10.33.0",
"scripts": { "scripts": {
@@ -15,7 +15,6 @@
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:extensions": "tsx scripts/postgres/enable-extensions.ts",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"postinstall": "cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/pdf.worker.min.mjs", "postinstall": "cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/pdf.worker.min.mjs",
"docker:up": "docker compose up -d", "docker:up": "docker compose up -d",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" role="img" aria-label="OpenMonetis">
<path fill="#ff7733" d="M 77.66,165.64 L 37.77,141.54 L 63.30,108.72 L 27.13,97.44 L 46.81,50.77 L 77.66,63.08 L 81.91,30.26 L 126.40,29.23 L 126.06,33.85 L 122.87,67.69 L 158.51,50.77 L 178.19,90.26 L 140.96,104.62 L 162.23,127.18 L 132.98,162.56 L 103.19,131.79 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -58,21 +58,26 @@ DATA_FILE="$BACKUP_DIR/openmonetis_${TIMESTAMP}.data.sql.gz"
log "Iniciando backup (modo: $DB_MODE)..." log "Iniciando backup (modo: $DB_MODE)..."
# Schemas relevantes do OpenMonetis (descarta lixo Supabase: auth, realtime, storage, vault, graphql, etc.)
SCHEMA_FLAGS=(--schema=public --schema=drizzle)
# --- Dump --- # --- Dump ---
if [[ "$DB_MODE" == "remote" ]]; then if [[ "$DB_MODE" == "remote" ]]; then
# --no-owner --no-privileges: necessário no Supabase (roles gerenciados internamente) # --no-owner --no-privileges: necessário no Supabase (roles gerenciados internamente)
pg_dump --format=custom --no-owner --no-privileges \ pg_dump --format=custom --no-owner --no-privileges \
"${SCHEMA_FLAGS[@]}" \
"$REMOTE_DB_URL" > "$DUMP_FILE" "$REMOTE_DB_URL" > "$DUMP_FILE"
pg_dump --no-owner --no-privileges \ pg_dump --no-owner --no-privileges \
"${SCHEMA_FLAGS[@]}" \
"$REMOTE_DB_URL" | gzip > "$SQL_FILE" "$REMOTE_DB_URL" | gzip > "$SQL_FILE"
elif [[ "$DB_MODE" == "docker" ]]; then elif [[ "$DB_MODE" == "docker" ]]; then
docker exec "$DOCKER_CONTAINER" pg_dump \ docker exec "$DOCKER_CONTAINER" pg_dump \
-U "$DOCKER_DB_USER" -Fc "$DOCKER_DB_NAME" > "$DUMP_FILE" -U "$DOCKER_DB_USER" -Fc "${SCHEMA_FLAGS[@]}" "$DOCKER_DB_NAME" > "$DUMP_FILE"
docker exec "$DOCKER_CONTAINER" pg_dump \ docker exec "$DOCKER_CONTAINER" pg_dump \
-U "$DOCKER_DB_USER" "$DOCKER_DB_NAME" | gzip > "$SQL_FILE" -U "$DOCKER_DB_USER" "${SCHEMA_FLAGS[@]}" "$DOCKER_DB_NAME" | gzip > "$SQL_FILE"
else else
log "ERRO: DB_MODE inválido ('$DB_MODE'). Use 'remote' ou 'docker'." log "ERRO: DB_MODE inválido ('$DB_MODE'). Use 'remote' ou 'docker'."

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

@@ -224,8 +224,8 @@ export function CategoryReportExport({
const doc = new jsPDF({ orientation: "landscape" }); const doc = new jsPDF({ orientation: "landscape" });
const primaryColor = getPrimaryPdfColor(); const primaryColor = getPrimaryPdfColor();
const [smallLogoDataUrl, textLogoDataUrl] = await Promise.all([ const [smallLogoDataUrl, textLogoDataUrl] = await Promise.all([
loadExportLogoDataUrl("/images/logo_small.png"), loadExportLogoDataUrl("/images/logo_small.svg"),
loadExportLogoDataUrl("/images/logo_text.png"), loadExportLogoDataUrl("/images/logo_text.svg"),
]); ]);
let brandingEndX = 14; let brandingEndX = 14;

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

@@ -229,8 +229,8 @@ export function TransactionsExport({
const doc = new jsPDF({ orientation: "landscape" }); const doc = new jsPDF({ orientation: "landscape" });
const primaryColor = getPrimaryPdfColor(); const primaryColor = getPrimaryPdfColor();
const [smallLogoDataUrl, textLogoDataUrl] = await Promise.all([ const [smallLogoDataUrl, textLogoDataUrl] = await Promise.all([
loadExportLogoDataUrl("/images/logo_small.png"), loadExportLogoDataUrl("/images/logo_small.svg"),
loadExportLogoDataUrl("/images/logo_text.png"), loadExportLogoDataUrl("/images/logo_text.svg"),
]); ]);
let brandingEndX = 14; let brandingEndX = 14;

View File

@@ -0,0 +1,18 @@
import type { SVGProps } from "react";
export function LogoIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 200 200"
role="img"
aria-label="OpenMonetis"
{...props}
>
<path
fill="#ff7733"
d="M 77.66,165.64 L 37.77,141.54 L 63.30,108.72 L 27.13,97.44 L 46.81,50.77 L 77.66,63.08 L 81.91,30.26 L 126.40,29.23 L 126.06,33.85 L 122.87,67.69 L 158.51,50.77 L 178.19,90.26 L 140.96,104.62 L 162.23,127.18 L 132.98,162.56 L 103.19,131.79 Z"
/>
</svg>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,5 @@
import Image from "next/image"; import { LogoIcon } from "@/shared/components/logo-icon";
import { LogoText } from "@/shared/components/logo-text";
import { cn } from "@/shared/utils/ui"; import { cn } from "@/shared/utils/ui";
interface LogoProps { interface LogoProps {
@@ -27,75 +28,39 @@ export function Logo({
if (variant === "compact") { if (variant === "compact") {
return ( return (
<div className={cn("flex items-center gap-1", className)}> <div className={cn("flex items-center gap-1", className)}>
<div className="relative size-8 shrink-0"> <LogoIcon
<Image
src="/images/logo_small.png"
alt="OpenMonetis"
fill
sizes="32px"
className={cn( className={cn(
"object-contain", "size-8 shrink-0",
!colorIcon && iconFilterClass, !colorIcon && iconFilterClass,
iconClassName, iconClassName,
)} )}
priority
/> />
</div> <LogoText
<div className="relative hidden h-8 w-[110px] shrink-0 sm:block">
<Image
src="/images/logo_text.png"
alt="OpenMonetis"
fill
sizes="110px"
className={cn( className={cn(
"object-contain", "hidden h-auto w-[110px] shrink-0 sm:block",
invertTextOnDark && "dark:invert", invertTextOnDark && "dark:invert",
textClassName, textClassName,
)} )}
priority
/> />
</div> </div>
</div>
); );
} }
if (variant === "small") { if (variant === "small") {
return ( return <LogoIcon className={cn("size-8 shrink-0", className)} />;
<div className={cn("relative size-8 shrink-0", className)}>
<Image
src="/images/logo_small.png"
alt="OpenMonetis"
fill
sizes="32px"
className="object-contain"
priority
/>
</div>
);
} }
return ( return (
<div className={cn("flex items-center gap-1.5 py-4", className)}> <div className={cn("flex items-center gap-1.5 py-4", className)}>
<div className="relative size-7 shrink-0"> <LogoIcon
<Image className={cn("size-7 shrink-0", !colorIcon && iconFilterClass)}
src="/images/logo_small.png"
alt="OpenMonetis"
fill
sizes="28px"
className={cn("object-contain", !colorIcon && iconFilterClass)}
priority
/> />
</div> <LogoText
<div className="relative h-8 w-[100px] shrink-0"> className={cn(
<Image "h-auto w-[100px] shrink-0",
src="/images/logo_text.png" invertTextOnDark && "dark:invert",
alt="OpenMonetis" )}
fill
sizes="100px"
className={cn("object-contain", invertTextOnDark && "dark:invert")}
priority
/> />
</div> </div>
</div>
); );
} }

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);
};

View File

@@ -65,8 +65,10 @@ export function getPrimaryPdfColor(): [number, number, number] {
return FALLBACK_PRIMARY_COLOR; return FALLBACK_PRIMARY_COLOR;
} }
const EXPORT_LOGO_RENDER_SCALE = 4;
export async function loadExportLogoDataUrl( export async function loadExportLogoDataUrl(
logoPath = "/images/logo_text.png", logoPath = "/images/logo_text.svg",
): Promise<string | null> { ): Promise<string | null> {
if (typeof window === "undefined" || typeof document === "undefined") { if (typeof window === "undefined" || typeof document === "undefined") {
return null; return null;
@@ -77,13 +79,16 @@ export async function loadExportLogoDataUrl(
image.crossOrigin = "anonymous"; image.crossOrigin = "anonymous";
image.onload = () => { image.onload = () => {
const width = image.naturalWidth || image.width; const naturalWidth = image.naturalWidth || image.width;
const height = image.naturalHeight || image.height; const naturalHeight = image.naturalHeight || image.height;
if (!width || !height) { if (!naturalWidth || !naturalHeight) {
resolve(null); resolve(null);
return; return;
} }
const width = Math.round(naturalWidth * EXPORT_LOGO_RENDER_SCALE);
const height = Math.round(naturalHeight * EXPORT_LOGO_RENDER_SCALE);
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;

View File

@@ -1,8 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"ignoreDeprecations": "6.0", "target": "ES2022",
"baseUrl": ".",
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,