Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ DEL_INSTANCE=false

# Provider: postgresql | mysql | psql_bouncer
DATABASE_PROVIDER=postgresql
DATABASE_CONNECTION_URI='postgresql://user:pass@evolution-postgres:5432/evolution_db?schema=evolution_api'
DATABASE_CONNECTION_URI=postgresql://user:pass@evolution-postgres:5432/evolution_db

# Postgres container settings (used by docker-compose)
POSTGRES_DATABASE=evolution_db
Expand Down
112 changes: 112 additions & 0 deletions Docker/scripts/create_database.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env node
/**
* Ensures the target database exists before running migrations.
* Connects to the server's default admin database, checks if the target
* database is present, and creates it if not.
*
* Runs as a non-fatal pre-migration step: if the user lacks CREATE DATABASE
* permission (managed DB, read-only replica, etc.) the error is logged as a
* warning and the process exits 0 so the migration attempt still proceeds.
*
* Supported providers: postgresql, psql_bouncer, mysql
*/

'use strict';

const provider = process.env.DATABASE_PROVIDER ?? 'postgresql';
const uri = process.env.DATABASE_CONNECTION_URI;

if (!uri) {
console.error('[create_database] DATABASE_CONNECTION_URI is not set — skipping auto-create.');
process.exit(0);
}

async function ensurePostgresDatabase() {
const { Pool } = require('pg');

const url = new URL(uri);
const dbName = decodeURIComponent(url.pathname.slice(1));

if (!dbName) {
console.warn('[create_database] Could not parse database name from URI — skipping.');
return;
}

// Connect to the server admin database to check / create the target DB
const adminUrl = new URL(uri);
adminUrl.pathname = '/postgres';
// Strip any search params that are schema-specific (not relevant for admin conn)
adminUrl.search = '';

const pool = new Pool({ connectionString: adminUrl.toString(), connectionTimeoutMillis: 10000 });

try {
const result = await pool.query('SELECT 1 FROM pg_database WHERE datname = $1', [dbName]);

if (result.rowCount === 0) {
console.log(`[create_database] Database "${dbName}" not found. Creating...`);
// Identifier cannot be parameterized — validate it is a plain name first
if (!/^[\w-]+$/.test(dbName)) {
throw new Error(`Refusing to CREATE DATABASE: name "${dbName}" contains unsafe characters.`);
}
await pool.query(`CREATE DATABASE "${dbName}"`);
console.log(`[create_database] Database "${dbName}" created successfully.`);
} else {
console.log(`[create_database] Database "${dbName}" already exists — skipping creation.`);
}
} finally {
await pool.end();
}
}

async function ensureMysqlDatabase() {
// mysql2 is a transitive dep via @prisma/adapter-mariadb; attempt to load it
let mysql;
try {
mysql = require('mysql2/promise');
} catch {
console.warn('[create_database] mysql2 not available — skipping auto-create for MySQL.');
return;
}

const url = new URL(uri);
const dbName = decodeURIComponent(url.pathname.slice(1));

if (!dbName) {
console.warn('[create_database] Could not parse database name from URI — skipping.');
return;
}

if (!/^[\w-]+$/.test(dbName)) {
throw new Error(`Refusing to CREATE DATABASE: name "${dbName}" contains unsafe characters.`);
}

const connection = await mysql.createConnection({
host: url.hostname,
port: Number(url.port) || 3306,
user: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
connectTimeout: 10000,
});

try {
await connection.execute(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
console.log(`[create_database] Database "${dbName}" ensured.`);
} finally {
await connection.end();
}
}

(async () => {
try {
if (provider === 'mysql') {
await ensureMysqlDatabase();
} else {
await ensurePostgresDatabase();
}
} catch (err) {
console.warn(`[create_database] Warning: could not auto-create database: ${err.message}`);
console.warn('[create_database] Proceeding — migration will fail with a clear error if the DB is truly missing.');
process.exit(0);
}
})();
2 changes: 2 additions & 0 deletions Docker/scripts/deploy_database.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ fi

if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" || "$DATABASE_PROVIDER" == "psql_bouncer" ]]; then
export DATABASE_URL
echo "Ensuring database exists for $DATABASE_PROVIDER..."
node ./Docker/scripts/create_database.js
echo "Deploying migrations for $DATABASE_PROVIDER"
echo "Database URL: $DATABASE_URL"
# rm -rf ./prisma/migrations
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ COPY ./prisma ./prisma
COPY ./manager ./manager
COPY ./.env.example ./.env
COPY ./runWithProvider.js ./
COPY ./prisma.config.ts ./

COPY ./Docker ./Docker

Expand Down Expand Up @@ -65,6 +66,7 @@ COPY --from=builder /evolution/.env ./.env
COPY --from=builder /evolution/Docker ./Docker
COPY --from=builder /evolution/runWithProvider.js ./runWithProvider.js
COPY --from=builder /evolution/tsup.config.ts ./tsup.config.ts
COPY --from=builder /evolution/prisma.config.ts ./prisma.config.ts

ENV DOCKER_ENV=true

Expand Down
1 change: 1 addition & 0 deletions patches/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

7 changes: 6 additions & 1 deletion src/api/abstract/abstract.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ export abstract class RouterBroker {
}

if (request.originalUrl.includes('/instance/create')) {
Object.assign(instance, sanitizeUntrustedInput(body));
const sanitized = sanitizeUntrustedInput(body);
// instanceName must come from the body on create — there is no URL param on this route
if (body?.instanceName !== undefined) {
sanitized.instanceName = body.instanceName;
}
Object.assign(instance, sanitized);
Comment on lines +54 to +59

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Overriding instanceName after sanitization may reintroduce unsafe data from the raw body.

By setting sanitized.instanceName = body.instanceName after calling sanitizeUntrustedInput, you circumvent any validation or filtering that function would apply to instanceName. If sanitizeUntrustedInput is responsible for enforcing allowed keys or validating value formats, consider either updating it to explicitly handle instanceName or adding explicit validation here before assigning, rather than copying the raw value.

}

Object.assign(ref, body);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class EvoHubControlPlaneRouter extends RouterBroker {
});

this.router.get('/evohub/channels/:id', guard, async (req, res) => {
res.json(await evoHubClient.getChannel(req.params.id));
res.json(await evoHubClient.getChannel(req.params.id as string));
});

this.router.get('/evohub/available-channels', guard, async (_req, res) => {
Expand Down Expand Up @@ -114,7 +114,7 @@ export class EvoHubControlPlaneRouter extends RouterBroker {
});

this.router.post('/evohub/channels/:id/meta-connect', guard, async (req, res) => {
res.json(await evoHubClient.connectToMeta(req.params.id, req.body));
res.json(await evoHubClient.connectToMeta(req.params.id as string, req.body));
});
}
}
17 changes: 17 additions & 0 deletions src/api/services/monitor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,25 @@ export class WAMonitoringService {
businessId: data.businessId,
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider wrapping instance creation and Setting upsert in a single transaction for atomicity.

If the Instance is created but the Setting upsert fails (e.g., transient DB error), you can still end up with an Instance without a Setting, which undermines the intent of this change. Wrapping both operations in this.prismaRepository.$transaction would keep them atomic and prevent this partial state.

Suggested implementation:

          businessId: data.businessId,
        },
      });
      await this.prismaRepository.$transaction(async (tx) => {
        // Ensure Instance creation and Setting record creation happen atomically.
        await tx.instance.create({
          data: {
            businessId: data.businessId,
          },
        });

        // Ensure Setting record exists immediately after Instance creation.
        // Prevents FK constraint violations (Setting_instanceId_fkey) in chatbot
        // integrations that write to IntegrationSession before setSettings() runs.
        await tx.setting.upsert({
          where: { instanceId: data.instanceId },
          update: {},
          create: {
            instanceId: data.instanceId,
            rejectCall: false,
            groupsIgnore: false,
            alwaysOnline: false,
            readMessages: false,
            readStatus: false,
            syncFullHistory: false,
          },
        });
      });

The tx.instance.create block in the replacement currently only includes businessId: data.businessId because only that part of the original data object was visible in the snippet. To avoid changing behavior, the full data payload used in the original this.prismaRepository.instance.create({ data: { ... } }) call must be copied into the new tx.instance.create({ data: { ... } }) block, preserving all fields that were previously set (e.g., any other properties before businessId).

// Ensure Setting record exists immediately after Instance creation.
// Prevents FK constraint violations (Setting_instanceId_fkey) in chatbot
// integrations that write to IntegrationSession before setSettings() runs.
await this.prismaRepository.setting.upsert({
where: { instanceId: data.instanceId },
update: {},
create: {
instanceId: data.instanceId,
rejectCall: false,
groupsIgnore: false,
alwaysOnline: false,
readMessages: false,
readStatus: false,
syncFullHistory: false,
},
});
} catch (error) {
this.logger.error(error);
throw error;
}
}

Expand Down
7 changes: 4 additions & 3 deletions tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ export default defineConfig({
// Apenas o nome exato @prisma/client é bundlado (redirecionado pelo alias para o client
// gerado). Subpaths como @prisma/client/runtime/* permanecem externos (node_modules).
noExternal: [/^@prisma\/client$/],
define: {
__LICENSE_ENDPOINT_ENCODED__: licenseEndpointEncoded,
__LICENSE_ENDPOINT_XOR_KEY__: licenseEndpointXorKey,
},
esbuildOptions(options) {
// platform node: garante o shim de import.meta.url no bundle CJS (client Prisma 7 usa import.meta)
options.platform = 'node';
options.alias = {
...(options.alias ?? {}),
'@prisma/client': path.resolve(process.cwd(), 'prisma/generated/client/client.ts'),
};
define: {
__LICENSE_ENDPOINT_ENCODED__: licenseEndpointEncoded,
__LICENSE_ENDPOINT_XOR_KEY__: licenseEndpointXorKey,
},
onSuccess: async () => {
cpSync('src/utils/translations', 'dist/translations', { recursive: true });
Expand Down
Loading