Extract backend provisioning service
This commit is contained in:
290
backend/index.js
290
backend/index.js
@@ -1,12 +1,12 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const pool = require('./db');
|
const pool = require('./db');
|
||||||
const transporter = require('./services/mailer');
|
const transporter = require('./services/mailer');
|
||||||
|
const { provisionSuperAdmin } = require('./services/provisioning');
|
||||||
const { createCorsMiddleware } = require('./config/cors');
|
const { createCorsMiddleware } = require('./config/cors');
|
||||||
const { allowedOrigins, getBaseUrl, getStartupBaseUrl, isProduction, jwtSecret: JWT_SECRET, port: PORT } = require('./config/runtime');
|
const { allowedOrigins, getBaseUrl, getStartupBaseUrl, isProduction, jwtSecret: JWT_SECRET, port: PORT } = require('./config/runtime');
|
||||||
const { authenticateToken } = require('./middleware/auth');
|
const { authenticateToken } = require('./middleware/auth');
|
||||||
@@ -110,293 +110,7 @@ if (process.env.NODE_ENV === 'production') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-provision Super Admin
|
|
||||||
const provisionSuperAdmin = async (retries = 10, delay = 10000) => {
|
|
||||||
const email = 'suporte@blyzer.com.br';
|
|
||||||
|
|
||||||
for (let i = 0; i < retries; i++) {
|
|
||||||
try {
|
|
||||||
// Test connection first
|
|
||||||
const connection = await pool.getConnection();
|
|
||||||
|
|
||||||
// Auto-create missing tables to prevent issues with outdated Docker configs/volumes
|
|
||||||
await connection.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS password_resets (
|
|
||||||
email varchar(255) NOT NULL,
|
|
||||||
token varchar(255) NOT NULL,
|
|
||||||
expires_at timestamp NOT NULL,
|
|
||||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (token),
|
|
||||||
KEY email (email)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await connection.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS pending_registrations (
|
|
||||||
email varchar(255) NOT NULL,
|
|
||||||
password_hash varchar(255) NOT NULL,
|
|
||||||
full_name varchar(255) NOT NULL,
|
|
||||||
organization_name varchar(255) NOT NULL,
|
|
||||||
verification_code varchar(10) NOT NULL,
|
|
||||||
expires_at timestamp NOT NULL,
|
|
||||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (email)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
`);
|
|
||||||
|
|
||||||
await connection.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS notifications (
|
|
||||||
id varchar(36) NOT NULL,
|
|
||||||
user_id varchar(36) NOT NULL,
|
|
||||||
type enum('success', 'info', 'warning', 'error') DEFAULT 'info',
|
|
||||||
title varchar(255) NOT NULL,
|
|
||||||
message text NOT NULL,
|
|
||||||
link varchar(255) DEFAULT NULL,
|
|
||||||
is_read boolean DEFAULT false,
|
|
||||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
KEY user_id (user_id),
|
|
||||||
KEY created_at (created_at)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Add slug column if it doesn't exist
|
|
||||||
try {
|
|
||||||
await connection.query('ALTER TABLE users ADD COLUMN slug VARCHAR(255) UNIQUE DEFAULT NULL');
|
|
||||||
} catch (err) {
|
|
||||||
// Ignore error if column already exists (ER_DUP_FIELDNAME)
|
|
||||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (slug):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate empty slugs
|
|
||||||
try {
|
|
||||||
await connection.query(`UPDATE users SET slug = CONCAT(LOWER(REPLACE(name, ' ', '-')), '-', SUBSTRING(MD5(RAND()), 1, 8)) WHERE slug IS NULL`);
|
|
||||||
} catch (err) {
|
|
||||||
console.log('Schema update note (populate slugs):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update origin to VARCHAR for custom origins
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE attendances MODIFY COLUMN origin VARCHAR(255) NOT NULL");
|
|
||||||
} catch (err) {
|
|
||||||
console.log('Schema update note (origin):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert funnel_stage to VARCHAR for custom funnels
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE attendances MODIFY COLUMN funnel_stage VARCHAR(255) NOT NULL");
|
|
||||||
} catch (err) {
|
|
||||||
console.log('Schema update note (funnel_stage):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add full_summary column for detailed AI analysis
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE attendances ADD COLUMN full_summary TEXT DEFAULT NULL");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (full_summary):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create origin_groups table
|
|
||||||
await connection.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS origin_groups (
|
|
||||||
id varchar(36) NOT NULL,
|
|
||||||
tenant_id varchar(36) NOT NULL,
|
|
||||||
name varchar(255) NOT NULL,
|
|
||||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
KEY tenant_id (tenant_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Create origin_items table
|
|
||||||
await connection.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS origin_items (
|
|
||||||
id varchar(36) NOT NULL,
|
|
||||||
origin_group_id varchar(36) NOT NULL,
|
|
||||||
name varchar(255) NOT NULL,
|
|
||||||
color_class varchar(255) DEFAULT 'bg-zinc-100 text-zinc-800 border-zinc-200',
|
|
||||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
KEY origin_group_id (origin_group_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Attempt to add color_class if table already existed without it
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE origin_items ADD COLUMN color_class VARCHAR(255) DEFAULT 'bg-zinc-100 text-zinc-800 border-zinc-200'");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (origin_items.color_class):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add origin_group_id to teams
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE teams ADD COLUMN origin_group_id VARCHAR(36) DEFAULT NULL");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (teams.origin_group_id):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename summary to title
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE attendances RENAME COLUMN summary TO title");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ER_BAD_FIELD_ERROR' && err.code !== 'ER_DUP_FIELDNAME') {
|
|
||||||
// If RENAME COLUMN fails (older mysql), try CHANGE
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE attendances CHANGE COLUMN summary title TEXT");
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Schema update note (summary to title):', e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create funnels table
|
|
||||||
await connection.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS funnels (
|
|
||||||
id varchar(36) NOT NULL,
|
|
||||||
tenant_id varchar(36) NOT NULL,
|
|
||||||
name varchar(255) NOT NULL,
|
|
||||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
KEY tenant_id (tenant_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Create funnel_stages table
|
|
||||||
await connection.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS funnel_stages (
|
|
||||||
id varchar(36) NOT NULL,
|
|
||||||
funnel_id varchar(36) NOT NULL,
|
|
||||||
name varchar(255) NOT NULL,
|
|
||||||
color_class varchar(255) DEFAULT 'bg-zinc-100 text-zinc-800 border-zinc-200',
|
|
||||||
order_index int DEFAULT 0,
|
|
||||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
KEY funnel_id (funnel_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Create api_keys table for external integrations (n8n)
|
|
||||||
await connection.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS api_keys (
|
|
||||||
id varchar(36) NOT NULL,
|
|
||||||
tenant_id varchar(36) NOT NULL,
|
|
||||||
name varchar(255) NOT NULL,
|
|
||||||
secret_key varchar(255) NOT NULL,
|
|
||||||
secret_hash varchar(64) DEFAULT NULL,
|
|
||||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_used_at timestamp NULL DEFAULT NULL,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY secret_key (secret_key),
|
|
||||||
UNIQUE KEY secret_hash (secret_hash),
|
|
||||||
KEY tenant_id (tenant_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE api_keys ADD COLUMN secret_hash VARCHAR(64) DEFAULT NULL");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (api_keys.secret_hash):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE api_keys ADD UNIQUE KEY secret_hash (secret_hash)");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ER_DUP_KEYNAME') console.log('Schema update note (api_keys.secret_hash index):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create refresh_tokens table for persistent sessions
|
|
||||||
await connection.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
|
||||||
id varchar(36) NOT NULL,
|
|
||||||
user_id varchar(36) NOT NULL,
|
|
||||||
token varchar(255) NOT NULL,
|
|
||||||
token_hash varchar(64) DEFAULT NULL,
|
|
||||||
expires_at timestamp NOT NULL,
|
|
||||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY token (token),
|
|
||||||
UNIQUE KEY token_hash (token_hash),
|
|
||||||
KEY user_id (user_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE refresh_tokens ADD COLUMN token_hash VARCHAR(64) DEFAULT NULL");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (refresh_tokens.token_hash):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE refresh_tokens ADD UNIQUE KEY token_hash (token_hash)");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ER_DUP_KEYNAME') console.log('Schema update note (refresh_tokens.token_hash index):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add funnel_id to teams
|
|
||||||
try {
|
|
||||||
await connection.query("ALTER TABLE teams ADD COLUMN funnel_id VARCHAR(36) DEFAULT NULL");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (teams.funnel_id):', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.release();
|
|
||||||
// Ensure system tenant exists
|
|
||||||
await pool.query('INSERT IGNORE INTO tenants (id, name, slug, admin_email, status) VALUES (?, ?, ?, ?, ?)', ['system', 'System Admin', 'system', email, 'active']);
|
|
||||||
|
|
||||||
const [existing] = await pool.query('SELECT id, password_hash FROM users WHERE email = ?', [email]);
|
|
||||||
if (existing.length === 0 || existing[0].password_hash === 'pending_setup') {
|
|
||||||
console.log('Provisioning default super_admin or resending email...');
|
|
||||||
|
|
||||||
if (existing.length === 0) {
|
|
||||||
const uid = `u_${crypto.randomUUID().split('-')[0]}`;
|
|
||||||
const placeholderHash = 'pending_setup';
|
|
||||||
const superAdminSlug = 'suporte-blyzer';
|
|
||||||
|
|
||||||
await pool.query(
|
|
||||||
'INSERT INTO users (id, tenant_id, name, email, password_hash, slug, role, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
||||||
[uid, 'system', 'Blyzer Suporte', email, placeholderHash, superAdminSlug, 'super_admin', 'active']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = crypto.randomBytes(32).toString('hex');
|
|
||||||
// Delete any old unused tokens for this email to prevent buildup
|
|
||||||
await pool.query('DELETE FROM password_resets WHERE email = ?', [email]);
|
|
||||||
|
|
||||||
await pool.query(
|
|
||||||
'INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 15 MINUTE))',
|
|
||||||
[email, token]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setupLink = `${getStartupBaseUrl()}/#/setup-account?token=${token}`;
|
|
||||||
console.log(`\n\n=== SUPER ADMIN SETUP LINK ===\n${setupLink}\n==============================\n\n`);
|
|
||||||
|
|
||||||
await transporter.sendMail({
|
|
||||||
from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`,
|
|
||||||
to: email,
|
|
||||||
subject: 'Conta Super Admin Criada - Fasto',
|
|
||||||
html: `
|
|
||||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 12px; background: #ffffff; color: #0f172a;">
|
|
||||||
<h2 style="color: #0f172a;">Conta Super Admin Gerada</h2>
|
|
||||||
<p style="color: #475569;">Sua conta de suporte (super_admin) foi criada no Fasto.</p>
|
|
||||||
<div style="text-align: center; margin: 30px 0;">
|
|
||||||
<a href="${setupLink}" style="background-color: #0f172a; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">Finalizar Cadastro</a>
|
|
||||||
</div>
|
|
||||||
<p style="font-size: 12px; color: #94a3b8;">Este link expira em 15 minutos.</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}).catch(err => console.error("Failed to send super_admin email:", err));
|
|
||||||
}
|
|
||||||
return; // Success, exit the retry loop
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to provision super_admin (Attempt ${i + 1}/${retries}):`, error.message);
|
|
||||||
if (i < retries - 1) {
|
|
||||||
await new Promise(res => setTimeout(res, delay));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
app.listen(PORT, async () => {
|
app.listen(PORT, async () => {
|
||||||
await provisionSuperAdmin();
|
await provisionSuperAdmin({ pool, transporter, getStartupBaseUrl });
|
||||||
console.log(`🚀 Servidor Backend rodando em http://localhost:${PORT}`);
|
console.log(`🚀 Servidor Backend rodando em http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
273
backend/services/provisioning.js
Normal file
273
backend/services/provisioning.js
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const provisionSuperAdmin = async ({ pool, transporter, getStartupBaseUrl, retries = 10, delay = 10000 }) => {
|
||||||
|
const email = 'suporte@blyzer.com.br';
|
||||||
|
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = await pool.getConnection();
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS password_resets (
|
||||||
|
email varchar(255) NOT NULL,
|
||||||
|
token varchar(255) NOT NULL,
|
||||||
|
expires_at timestamp NOT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (token),
|
||||||
|
KEY email (email)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS pending_registrations (
|
||||||
|
email varchar(255) NOT NULL,
|
||||||
|
password_hash varchar(255) NOT NULL,
|
||||||
|
full_name varchar(255) NOT NULL,
|
||||||
|
organization_name varchar(255) NOT NULL,
|
||||||
|
verification_code varchar(10) NOT NULL,
|
||||||
|
expires_at timestamp NOT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (email)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS notifications (
|
||||||
|
id varchar(36) NOT NULL,
|
||||||
|
user_id varchar(36) NOT NULL,
|
||||||
|
type enum('success', 'info', 'warning', 'error') DEFAULT 'info',
|
||||||
|
title varchar(255) NOT NULL,
|
||||||
|
message text NOT NULL,
|
||||||
|
link varchar(255) DEFAULT NULL,
|
||||||
|
is_read boolean DEFAULT false,
|
||||||
|
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY user_id (user_id),
|
||||||
|
KEY created_at (created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query('ALTER TABLE users ADD COLUMN slug VARCHAR(255) UNIQUE DEFAULT NULL');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (slug):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query(`UPDATE users SET slug = CONCAT(LOWER(REPLACE(name, ' ', '-')), '-', SUBSTRING(MD5(RAND()), 1, 8)) WHERE slug IS NULL`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Schema update note (populate slugs):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE attendances MODIFY COLUMN origin VARCHAR(255) NOT NULL");
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Schema update note (origin):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE attendances MODIFY COLUMN funnel_stage VARCHAR(255) NOT NULL");
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Schema update note (funnel_stage):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE attendances ADD COLUMN full_summary TEXT DEFAULT NULL");
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (full_summary):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS origin_groups (
|
||||||
|
id varchar(36) NOT NULL,
|
||||||
|
tenant_id varchar(36) NOT NULL,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY tenant_id (tenant_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS origin_items (
|
||||||
|
id varchar(36) NOT NULL,
|
||||||
|
origin_group_id varchar(36) NOT NULL,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
color_class varchar(255) DEFAULT 'bg-zinc-100 text-zinc-800 border-zinc-200',
|
||||||
|
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY origin_group_id (origin_group_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE origin_items ADD COLUMN color_class VARCHAR(255) DEFAULT 'bg-zinc-100 text-zinc-800 border-zinc-200'");
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (origin_items.color_class):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE teams ADD COLUMN origin_group_id VARCHAR(36) DEFAULT NULL");
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (teams.origin_group_id):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE attendances RENAME COLUMN summary TO title");
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ER_BAD_FIELD_ERROR' && err.code !== 'ER_DUP_FIELDNAME') {
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE attendances CHANGE COLUMN summary title TEXT");
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Schema update note (summary to title):', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS funnels (
|
||||||
|
id varchar(36) NOT NULL,
|
||||||
|
tenant_id varchar(36) NOT NULL,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY tenant_id (tenant_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS funnel_stages (
|
||||||
|
id varchar(36) NOT NULL,
|
||||||
|
funnel_id varchar(36) NOT NULL,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
color_class varchar(255) DEFAULT 'bg-zinc-100 text-zinc-800 border-zinc-200',
|
||||||
|
order_index int DEFAULT 0,
|
||||||
|
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY funnel_id (funnel_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS api_keys (
|
||||||
|
id varchar(36) NOT NULL,
|
||||||
|
tenant_id varchar(36) NOT NULL,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
secret_key varchar(255) NOT NULL,
|
||||||
|
secret_hash varchar(64) DEFAULT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_used_at timestamp NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY secret_key (secret_key),
|
||||||
|
UNIQUE KEY secret_hash (secret_hash),
|
||||||
|
KEY tenant_id (tenant_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE api_keys ADD COLUMN secret_hash VARCHAR(64) DEFAULT NULL");
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (api_keys.secret_hash):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE api_keys ADD UNIQUE KEY secret_hash (secret_hash)");
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ER_DUP_KEYNAME') console.log('Schema update note (api_keys.secret_hash index):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||||
|
id varchar(36) NOT NULL,
|
||||||
|
user_id varchar(36) NOT NULL,
|
||||||
|
token varchar(255) NOT NULL,
|
||||||
|
token_hash varchar(64) DEFAULT NULL,
|
||||||
|
expires_at timestamp NOT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY token (token),
|
||||||
|
UNIQUE KEY token_hash (token_hash),
|
||||||
|
KEY user_id (user_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE refresh_tokens ADD COLUMN token_hash VARCHAR(64) DEFAULT NULL");
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (refresh_tokens.token_hash):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE refresh_tokens ADD UNIQUE KEY token_hash (token_hash)");
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ER_DUP_KEYNAME') console.log('Schema update note (refresh_tokens.token_hash index):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.query("ALTER TABLE teams ADD COLUMN funnel_id VARCHAR(36) DEFAULT NULL");
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (teams.funnel_id):', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.release();
|
||||||
|
connection = null;
|
||||||
|
|
||||||
|
await pool.query('INSERT IGNORE INTO tenants (id, name, slug, admin_email, status) VALUES (?, ?, ?, ?, ?)', ['system', 'System Admin', 'system', email, 'active']);
|
||||||
|
|
||||||
|
const [existing] = await pool.query('SELECT id, password_hash FROM users WHERE email = ?', [email]);
|
||||||
|
if (existing.length === 0 || existing[0].password_hash === 'pending_setup') {
|
||||||
|
console.log('Provisioning default super_admin or resending email...');
|
||||||
|
|
||||||
|
if (existing.length === 0) {
|
||||||
|
const uid = `u_${crypto.randomUUID().split('-')[0]}`;
|
||||||
|
const placeholderHash = 'pending_setup';
|
||||||
|
const superAdminSlug = 'suporte-blyzer';
|
||||||
|
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO users (id, tenant_id, name, email, password_hash, slug, role, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
|
[uid, 'system', 'Blyzer Suporte', email, placeholderHash, superAdminSlug, 'super_admin', 'active']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = crypto.randomBytes(32).toString('hex');
|
||||||
|
await pool.query('DELETE FROM password_resets WHERE email = ?', [email]);
|
||||||
|
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 15 MINUTE))',
|
||||||
|
[email, token]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setupLink = `${getStartupBaseUrl()}/#/setup-account?token=${token}`;
|
||||||
|
console.log(`\n\n=== SUPER ADMIN SETUP LINK ===\n${setupLink}\n==============================\n\n`);
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`,
|
||||||
|
to: email,
|
||||||
|
subject: 'Conta Super Admin Criada - Fasto',
|
||||||
|
html: `
|
||||||
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 12px; background: #ffffff; color: #0f172a;">
|
||||||
|
<h2 style="color: #0f172a;">Conta Super Admin Gerada</h2>
|
||||||
|
<p style="color: #475569;">Sua conta de suporte (super_admin) foi criada no Fasto.</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="${setupLink}" style="background-color: #0f172a; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">Finalizar Cadastro</a>
|
||||||
|
</div>
|
||||||
|
<p style="font-size: 12px; color: #94a3b8;">Este link expira em 15 minutos.</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}).catch(err => console.error("Failed to send super_admin email:", err));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (connection) connection.release();
|
||||||
|
console.error(`Failed to provision super_admin (Attempt ${i + 1}/${retries}):`, error.message);
|
||||||
|
if (i < retries - 1) {
|
||||||
|
await new Promise(res => setTimeout(res, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
provisionSuperAdmin,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user