Compare commits

..

14 Commits

Author SHA1 Message Date
Cauê Faleiros
509ed4a0d9 update docker compose again uhuu
All checks were successful
Build and Deploy / build-and-push (push) Successful in 57s
2026-03-25 16:43:07 -03:00
07cb43d0e3 revert b8541c0d24
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m2s
revert update docker compose again
2026-03-25 19:42:03 +00:00
Cauê Faleiros
b8541c0d24 update docker compose again
Some checks failed
Build and Deploy / build-and-push (push) Has been cancelled
2026-03-25 16:39:21 -03:00
Cauê Faleiros
062864364a update docker compose
Some checks failed
Build and Deploy / build-and-push (push) Has been cancelled
2026-03-25 16:38:23 -03:00
Cauê Faleiros
d148984028 chore: remove temporary postgres testing service
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m6s
- The portainer webhook testing service fulfilled its purpose and is no longer needed.
2026-03-25 15:03:39 -03:00
Cauê Faleiros
fa683ab28c chore: add dummy postgres service for portainer debugging
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m9s
- Added a simple postgres service to test if Portainer is successfully pulling and applying stack updates from the repository.
2026-03-25 14:48:24 -03:00
Cauê Faleiros
83e6da2d56 fix: add missing swarm deploy block to mysql backup service
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m9s
- Docker Swarm (Portainer) ignores 'restart: always' and often requires a 'deploy' block with replica counts to properly schedule a new service.

- Replaced standard docker-compose restart policy with a Swarm-compliant deploy block.
2026-03-25 14:43:03 -03:00
Cauê Faleiros
4dbd7c62cd fix: replace deprecated mysql backup image with modern cron backup container
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m5s
- The 'databack/mysql-backup' image crashes immediately when used in a pure cron context without legacy arguments and is incompatible with MySQL 8 caching_sha2_password.

- Switched to the actively maintained 'fradelg/mysql-cron-backup' image.

- Re-mapped environment variables to match the new image expectations (MYSQL_HOST, CRON_TIME, MAX_BACKUPS).

- Updated volume mapping destination to '/backup' as expected by the new image.
2026-03-25 14:34:36 -03:00
Cauê Faleiros
b5c8e97701 fix: revert backup volume path to exact specification
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m2s
- Updated the host bind mount path to exactly '/opt/backups_db' as explicitly required by the server configuration.
2026-03-25 14:20:05 -03:00
Cauê Faleiros
f65ff97434 fix: change mysql backup volume path to prevent permission denied errors
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m7s
- Updated the host bind mount from the restricted '/root' directory to '/opt/fasto_backups' to ensure the Docker daemon has the necessary permissions to read and write database dumps.
2026-03-25 14:18:16 -03:00
Cauê Faleiros
958a2cdbd9 chore: force recreate database init sql config in docker swarm
All checks were successful
Build and Deploy / build-and-push (push) Successful in 58s
- Appended a version suffix to the `init_sql` config name to bypass Docker Swarm's immutable config cache and force the cluster to pick up the latest database schema changes on deployment.
2026-03-25 13:55:31 -03:00
Cauê Faleiros
eb483f903b chore: remove swarm deployment constraint from mysql backup service
All checks were successful
Build and Deploy / build-and-push (push) Successful in 58s
- Dropped the 'node.role == manager' deployment constraint to allow the backup container to be scheduled on any available node or to run smoothly in non-swarm Docker Compose environments.
2026-03-25 13:23:34 -03:00
Cauê Faleiros
9ffcfcdcc8 chore: add automated database backup service and tighten backend security
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m56s
- Added `databack/mysql-backup` service to the production docker-compose Swarm stack, scheduling a daily 02:55 AM cron backup of the database with a 3-day local retention policy.

- Fixed a critical race condition in the backend JWT authentication middleware where an invalid token returning 401 could crash the response flow if the route executed before the defensive checks caught it.

- Added strict undefined defensive checks to the `getUserById` endpoint and RBAC middleware to gracefully reject requests that somehow bypass the token parser.

- Updated `GEMINI.md` technical documentation to fully match the real codebase logic.

- Fixed UX rule to prevent `manager` role from seeing Funnels or Origins tabs in the sidebar.

- Blocked `agent` role from modifying their own 'fullName' string in the Profile UI.
2026-03-25 12:40:53 -03:00
Cauê Faleiros
3663d03cb9 refactor(rbac): complete the removal of 'owner' role from backend routes and logic
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m48s
- Cleaned up the requireRole middleware across all Funnel and Origin API routes to strictly allow only 'admin' and 'super_admin' to perform structural changes.

- Updated the tenant creation script to assign the 'admin' role to new signups instead of 'owner'.
2026-03-23 15:40:36 -03:00
6 changed files with 207 additions and 79 deletions

162
App.tsx
View File

@@ -1,34 +1,48 @@
import React, { useState, useEffect } from 'react';
import { HashRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { Layout } from './components/Layout';
import { Dashboard } from './pages/Dashboard';
import { UserDetail } from './pages/UserDetail';
import { AttendanceDetail } from './pages/AttendanceDetail';
import { SuperAdmin } from './pages/SuperAdmin';
import { ApiKeys } from './pages/ApiKeys';
import { TeamManagement } from './pages/TeamManagement';
import { Teams } from './pages/Teams';
import { Funnels } from './pages/Funnels';
import { Origins } from './pages/Origins';
import { Login } from './pages/Login';
import { ForgotPassword } from './pages/ForgotPassword';
import { ResetPassword } from './pages/ResetPassword';
import { SetupAccount } from './pages/SetupAccount';
import { UserProfile } from './pages/UserProfile';
import { getUserById, logout } from './services/dataService';
import { User } from './types';
import React, { useState, useEffect } from "react";
import {
HashRouter as Router,
Routes,
Route,
Navigate,
useLocation,
} from "react-router-dom";
import { Layout } from "./components/Layout";
import { Dashboard } from "./pages/Dashboard";
import { UserDetail } from "./pages/UserDetail";
import { AttendanceDetail } from "./pages/AttendanceDetail";
import { SuperAdmin } from "./pages/SuperAdmin";
import { ApiKeys } from "./pages/ApiKeys";
import { TeamManagement } from "./pages/TeamManagement";
import { Teams } from "./pages/Teams";
import { Funnels } from "./pages/Funnels";
import { Origins } from "./pages/Origins";
import { Login } from "./pages/Login";
import { ForgotPassword } from "./pages/ForgotPassword";
import { ResetPassword } from "./pages/ResetPassword";
import { SetupAccount } from "./pages/SetupAccount";
import { UserProfile } from "./pages/UserProfile";
import { getUserById, logout } from "./services/dataService";
import { User } from "./types";
const AuthGuard: React.FC<{ children: React.ReactNode, roles?: string[] }> = ({ children, roles }) => {
const AuthGuard: React.FC<{ children: React.ReactNode; roles?: string[] }> = ({
children,
roles,
}) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const location = useLocation();
useEffect(() => {
const checkAuth = async () => {
const storedUserId = localStorage.getItem('ctms_user_id');
const storedToken = localStorage.getItem('ctms_token');
const storedUserId = localStorage.getItem("ctms_user_id");
const storedToken = localStorage.getItem("ctms_token");
if (!storedUserId || !storedToken || storedToken === 'undefined' || storedToken === 'null') {
if (
!storedUserId ||
!storedToken ||
storedToken === "undefined" ||
storedToken === "null"
) {
if (storedToken) logout(); // Limpar se for "undefined" string
setLoading(false);
return;
@@ -37,7 +51,7 @@ const AuthGuard: React.FC<{ children: React.ReactNode, roles?: string[] }> = ({
try {
const fetchedUser = await getUserById(storedUserId);
if (fetchedUser) {
if (fetchedUser.status === 'active') {
if (fetchedUser.status === "active") {
setUser(fetchedUser);
} else {
// User explicitly marked inactive or deleted
@@ -53,7 +67,7 @@ const AuthGuard: React.FC<{ children: React.ReactNode, roles?: string[] }> = ({
}
} catch (err) {
console.error("Auth check failed (network/server error):", err);
// DO NOT logout() here. If the server is offline or restarting,
// DO NOT logout() here. If the server is offline or restarting,
// we shouldn't wipe the user's local storage tokens.
// We just leave the user as null, which will redirect them to login,
// but their tokens remain so they can auto-login when the server is back.
@@ -66,7 +80,11 @@ const AuthGuard: React.FC<{ children: React.ReactNode, roles?: string[] }> = ({
}, [location.pathname]);
if (loading) {
return <div className="flex h-screen items-center justify-center bg-zinc-50 dark:bg-zinc-950 text-zinc-400">Carregando...</div>;
return (
<div className="flex h-screen items-center justify-center bg-zinc-50 dark:bg-zinc-950 text-zinc-400">
Carregando...
</div>
);
}
if (!user) {
@@ -78,7 +96,7 @@ const AuthGuard: React.FC<{ children: React.ReactNode, roles?: string[] }> = ({
}
// Auto-redirect Super Admins away from the standard dashboard to their specific panel
if (location.pathname === '/' && user.role === 'super_admin') {
if (location.pathname === "/" && user.role === "super_admin") {
return <Navigate to="/super-admin" replace />;
}
@@ -93,16 +111,86 @@ const App: React.FC = () => {
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/setup-account" element={<SetupAccount />} />
<Route path="/" element={<AuthGuard><Dashboard /></AuthGuard>} />
<Route path="/admin/users" element={<AuthGuard roles={['super_admin', 'admin', 'manager']}><TeamManagement /></AuthGuard>} />
<Route path="/admin/teams" element={<AuthGuard roles={['super_admin', 'admin', 'manager']}><Teams /></AuthGuard>} />
<Route path="/admin/funnels" element={<AuthGuard roles={['super_admin', 'admin', 'manager']}><Funnels /></AuthGuard>} />
<Route path="/admin/origins" element={<AuthGuard roles={['super_admin', 'admin', 'manager']}><Origins /></AuthGuard>} />
<Route path="/users/:id" element={<AuthGuard><UserDetail /></AuthGuard>} />
<Route path="/attendances/:id" element={<AuthGuard><AttendanceDetail /></AuthGuard>} />
<Route path="/super-admin" element={<AuthGuard roles={['super_admin']}><SuperAdmin /></AuthGuard>} />
<Route path="/super-admin/api-keys" element={<AuthGuard roles={['super_admin']}><ApiKeys /></AuthGuard>} />
<Route path="/profile" element={<AuthGuard><UserProfile /></AuthGuard>} />
<Route
path="/"
element={
<AuthGuard>
<Dashboard />
</AuthGuard>
}
/>
<Route
path="/admin/users"
element={
<AuthGuard roles={["super_admin", "admin", "manager"]}>
<TeamManagement />
</AuthGuard>
}
/>
<Route
path="/admin/teams"
element={
<AuthGuard roles={["super_admin", "admin", "manager"]}>
<Teams />
</AuthGuard>
}
/>
<Route
path="/admin/funnels"
element={
<AuthGuard roles={["super_admin", "admin"]}>
<Funnels />
</AuthGuard>
}
/>
<Route
path="/admin/origins"
element={
<AuthGuard roles={["super_admin", "admin"]}>
<Origins />
</AuthGuard>
}
/>
<Route
path="/users/:id"
element={
<AuthGuard>
<UserDetail />
</AuthGuard>
}
/>
<Route
path="/attendances/:id"
element={
<AuthGuard>
<AttendanceDetail />
</AuthGuard>
}
/>
<Route
path="/super-admin"
element={
<AuthGuard roles={["super_admin"]}>
<SuperAdmin />
</AuthGuard>
}
/>
<Route
path="/super-admin/api-keys"
element={
<AuthGuard roles={["super_admin"]}>
<ApiKeys />
</AuthGuard>
}
/>
<Route
path="/profile"
element={
<AuthGuard>
<UserProfile />
</AuthGuard>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Router>

View File

@@ -7,38 +7,44 @@ Fasto is a commercial team management system built with React (Vite) on the fron
We have transitioned from a mock-based prototype to a **secure, multi-tenant production architecture**:
- **Multi-Tenancy & Data Isolation:** All backend routes (Users, Teams, Attendances) now strictly enforce `tenant_id` checks. It is technically impossible for one organization to query data from another.
- **God Mode (Tenant Impersonation):** Super Admins can securely impersonate Tenant Admins via a specialized, temporary JWT (`/api/impersonate/:tenantId`). This allows seamless cross-domain support without storing passwords, while strictly maintaining state isolation through forced React reloads and locking mechanisms.
- **Dynamic Funnel Customization:**
- Funnel stages are no longer hardcoded ENUMs. Each tenant can create multiple dynamic funnels via the `funnels` and `funnel_stages` tables.
- Managers can assign specific Teams to specific Funnels.
- The Dashboard, User Detail, and Attendance Detail pages now dynamically map and color-code stages based on the active team's assigned funnel.
- **Real-Time Notification System:**
- Built a persistent notification tray (`/api/notifications`) with real-time polling (10s intervals) and a hidden HTML5 `<audio>` player for cross-browser sound playback.
- Automated Triggers: Super Admins are notified of new organizations; Tenant Admins/Managers are notified of new user setups; Users are notified of team assignment changes.
- **Role-Based Access Control (RBAC):**
- **Super Admin:** Global management of all tenants and users (via the hidden `system` tenant).
- **Admin/Manager:** Full control over members and teams within their specific organization.
- **Advanced 2-Token Authentication (Rolling Sessions):**
- Replaced the vulnerable 1-year static JWT with a highly secure dual-token system.
- Generates a short-lived `AccessToken` (15 min) and a stateful `RefreshToken` (30 days) stored in the DB (`refresh_tokens` table).
- Built an Axios-like `apiFetch` interceptor on the frontend that automatically catches 401 Unauthorized errors, fetches a new Access Token in the background, extends the Refresh Token by another 30 days (Sliding Expiration), and retries the original request without logging the user out.
- Full remote revocation capability (Logout drops the token from the DB immediately).
- **God Mode (Tenant Impersonation):** Super Admins can securely impersonate Tenant Admins via a specialized, temporary JWT (`/api/impersonate/:tenantId`). This allows seamless cross-domain support without storing passwords.
- **Role-Based Access Control (RBAC) Simplification:**
- Removed the redundant 'owner' role. The system now strictly relies on 4 tiers:
- **Super Admin:** Global management of all tenants and API keys (via the hidden `system` tenant).
- **Admin:** Full control over members, teams, funnels, and origins within their specific organization.
- **Manager:** Mid-level control. Can edit basic info of users in their specific team, but cannot change user roles or re-assign users to different teams (only Admins can).
- **Agent:** Restricted access. Can only view their own performance metrics and historical attendances.
- **Dynamic Funnel & Origin Managers:**
- Funnel stages and Lead Origins are no longer hardcoded ENUMs. Each tenant can create multiple dynamic funnel/origin groups via relational tables (`funnels`, `funnel_stages`, `origin_groups`, `origin_items`).
- Admins can customize the exact Tailwind color class (e.g., "bg-green-100") for each stage and origin via visual UI pickers.
- Admins assign specific Teams to specific Funnels/Origin Groups.
- The Dashboard pie charts and data tables strictly filter and color-code data based on the active team's configuration. Deleted data falls back to an "Outros" category to prevent chart breakage.
- **n8n / External API Webhooks (Completed):**
- Super Admins can generate persistent `api_keys` for specific tenants.
- `GET /api/integration/users`, `/funnels`, and `/origins` allow the n8n AI to dynamically map the tenant's actual agents and workflow stages before processing a chat.
- `POST /api/integration/attendances` accepts the AI's final JSON payload (including the `full_summary` text) and injects it directly into the dashboard.
- **Real-Time Notification System:**
- Built a persistent notification tray (`/api/notifications`) with real-time polling (10s intervals) and a hidden HTML5 `<audio>` player for cross-browser sound playback (custom `.mp3` loaded via Vite).
- Automated Triggers: Super Admins are notified of new organizations; Admins/Super Admins are notified of new user setups; Agents are notified of team assignment changes; Managers get "Venda Fechada" alerts when n8n posts a converted lead.
- **Enhanced UI/UX:**
- Premium "Onyx & Gold" True Black dark mode.
- Premium "Onyx & Gold" True Black dark mode (Zinc scale).
- Fully collapsible interactive sidebar with memory (`localStorage`).
- Action buttons across all data tables are permanently visible for faster discoverability (removed hover requirements).
- Loading states embedded directly into action buttons to prevent double-submissions.
- **Secure File Uploads:** Profile avatars use `multer` with strict mimetype validation (JPG/PNG/WEBP), 2MB size limits, and UUID generation.
- All Date/Time displays localized to strict Brazilian formatting (`pt-BR`, 24h, `DD/MM/YY`).
## 📌 Roadmap / To-Do
- [ ] **n8n / External API Integration (Priority 1):**
- Create an `api_keys` table to allow generating persistent, secure API keys for each Tenant.
- Build `GET /api/integration/users` so n8n can map its chat agents to Fasto's internal `user_id`s.
- Build `POST /api/integration/attendances` to allow n8n to programmatically create new attendances linked to specific tenants and users.
- [ ] **Sales & Quality Notification Triggers:** Implement backend logic to automatically notify Managers when an attendance is marked as "Won" (Ganhos), receives a critically low quality score, or breaches a specific Response Time SLA.
- [ ] **Advanced AI Notification Triggers:** Implement backend logic to automatically notify Managers when an attendance payload from n8n receives a critically low quality score (`score < 50`), or breaches a specific Response Time SLA (e.g., `first_response_time_min > 60`).
- [ ] **Data Export/Reporting:** Allow Admins to export attendance and KPI data to CSV/Excel.
- [ ] **Billing/Subscription Management:** Integrate a payment gateway (e.g., Stripe/Asaas) to manage tenant trial periods and active statuses dynamically.
## 🛠 Architecture
- **Frontend**: React 19, TypeScript, Vite, TailwindCSS (CDN), Recharts, Lucide React.
- **Backend**: Node.js, Express, MySQL2 (Pool-based), Nodemailer.
- **Database**: MySQL 8.0 (Schema: `fasto_db`).
- **Database**: MySQL 8.0 (Schema: `fasto_db` or `agenciac_comia` depending on `.env`).
- **Deployment**: Docker Compose for local development; Gitea Actions for CI/CD pushing to a Gitea Registry and deploying via Portainer webhook.
## 📋 Prerequisites
@@ -55,7 +61,7 @@ cp .env.example .env
*Note:* The backend automatically strips literal quotes from Docker `.env` string values (like `SMTP_PASS`) to prevent authentication crashes.
### 2. Database
The project expects a MySQL database. The `docker-compose.local.yml` initializes it. The Node.js backend automatically runs non-destructive schema migrations on startup (adding tables like `notifications`, `funnels`, `funnel_stages`, and modifying `attendances`).
The project expects a MySQL database. The Node.js backend automatically runs non-destructive schema migrations on startup (adding tables like `refresh_tokens`, `api_keys`, `origin_groups`, etc.).
### 3. Running Locally (Docker Compose)
To start the application and database locally:

View File

@@ -136,14 +136,17 @@ const authenticateToken = async (req, res, next) => {
if (!token) return res.status(401).json({ error: 'Token não fornecido.' });
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.status(401).json({ error: 'Token inválido ou expirado.' });
try {
const user = jwt.verify(token, JWT_SECRET);
req.user = user;
next();
});
} catch (err) {
return res.status(401).json({ error: 'Token inválido ou expirado.' });
}
};
const requireRole = (roles) => (req, res, next) => {
if (!req.user || !req.user.role) return res.status(401).json({ error: 'Não autenticado.' });
if (!roles.includes(req.user.role)) return res.status(403).json({ error: 'Acesso negado. Você não tem permissão para realizar esta ação.' });
next();
};
@@ -457,7 +460,7 @@ apiRouter.get('/users/:idOrSlug', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM users WHERE id = ? OR slug = ?', [req.params.idOrSlug, req.params.idOrSlug]);
if (!rows || rows.length === 0) return res.status(404).json({ error: 'Not found' });
if (!req.user) return res.status(401).json({ error: 'Não autenticado' });
if (!req.user || !req.user.role) return res.status(401).json({ error: 'Não autenticado' });
if (req.user.role !== 'super_admin' && rows[0].tenant_id !== req.user.tenant_id) {
return res.status(403).json({ error: 'Acesso negado.' });
@@ -734,7 +737,7 @@ apiRouter.get('/origins', async (req, res) => {
}
});
apiRouter.post('/origins', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.post('/origins', requireRole(['admin', 'super_admin']), async (req, res) => {
const { name, tenantId } = req.body;
const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id;
try {
@@ -746,7 +749,7 @@ apiRouter.post('/origins', requireRole(['admin', 'manager', 'super_admin']), asy
}
});
apiRouter.put('/origins/:id', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.put('/origins/:id', requireRole(['admin', 'super_admin']), async (req, res) => {
const { name, teamIds } = req.body;
try {
if (name) {
@@ -764,7 +767,7 @@ apiRouter.put('/origins/:id', requireRole(['admin', 'manager', 'super_admin']),
}
});
apiRouter.delete('/origins/:id', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.delete('/origins/:id', requireRole(['admin', 'super_admin']), async (req, res) => {
try {
await pool.query('DELETE FROM origin_items WHERE origin_group_id = ?', [req.params.id]);
await pool.query('UPDATE teams SET origin_group_id = NULL WHERE origin_group_id = ?', [req.params.id]);
@@ -775,7 +778,7 @@ apiRouter.delete('/origins/:id', requireRole(['admin', 'manager', 'super_admin']
}
});
apiRouter.post('/origins/:id/items', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.post('/origins/:id/items', requireRole(['admin', 'super_admin']), async (req, res) => {
const { name, color_class } = req.body;
try {
const oid = `oriitm_${crypto.randomUUID().split('-')[0]}`;
@@ -789,7 +792,7 @@ apiRouter.post('/origins/:id/items', requireRole(['admin', 'manager', 'super_adm
}
});
apiRouter.put('/origin_items/:id', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.put('/origin_items/:id', requireRole(['admin', 'super_admin']), async (req, res) => {
const { name, color_class } = req.body;
try {
const [existing] = await pool.query('SELECT * FROM origin_items WHERE id = ?', [req.params.id]);
@@ -802,7 +805,7 @@ apiRouter.put('/origin_items/:id', requireRole(['admin', 'manager', 'super_admin
}
});
apiRouter.delete('/origin_items/:id', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.delete('/origin_items/:id', requireRole(['admin', 'super_admin']), async (req, res) => {
try {
await pool.query('DELETE FROM origin_items WHERE id = ?', [req.params.id]);
res.json({ message: 'Origin item deleted.' });
@@ -863,7 +866,7 @@ apiRouter.get('/funnels', async (req, res) => {
}
});
apiRouter.post('/funnels', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.post('/funnels', requireRole(['admin', 'super_admin']), async (req, res) => {
const { name, tenantId } = req.body;
const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id;
try {
@@ -875,7 +878,7 @@ apiRouter.post('/funnels', requireRole(['admin', 'manager', 'super_admin']), asy
}
});
apiRouter.put('/funnels/:id', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.put('/funnels/:id', requireRole(['admin', 'super_admin']), async (req, res) => {
const { name, teamIds } = req.body;
try {
if (name) {
@@ -893,7 +896,7 @@ apiRouter.put('/funnels/:id', requireRole(['admin', 'manager', 'super_admin']),
}
});
apiRouter.delete('/funnels/:id', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.delete('/funnels/:id', requireRole(['admin', 'super_admin']), async (req, res) => {
try {
await pool.query('DELETE FROM funnel_stages WHERE funnel_id = ?', [req.params.id]);
await pool.query('UPDATE teams SET funnel_id = NULL WHERE funnel_id = ?', [req.params.id]);
@@ -904,7 +907,7 @@ apiRouter.delete('/funnels/:id', requireRole(['admin', 'manager', 'super_admin']
}
});
apiRouter.post('/funnels/:id/stages', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.post('/funnels/:id/stages', requireRole(['admin', 'super_admin']), async (req, res) => {
const { name, color_class, order_index } = req.body;
try {
const sid = `stage_${crypto.randomUUID().split('-')[0]}`;
@@ -918,7 +921,7 @@ apiRouter.post('/funnels/:id/stages', requireRole(['admin', 'manager', 'super_ad
}
});
apiRouter.put('/funnel_stages/:id', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.put('/funnel_stages/:id', requireRole(['admin', 'super_admin']), async (req, res) => {
const { name, color_class, order_index } = req.body;
try {
const [existing] = await pool.query('SELECT * FROM funnel_stages WHERE id = ?', [req.params.id]);
@@ -934,7 +937,7 @@ apiRouter.put('/funnel_stages/:id', requireRole(['admin', 'manager', 'super_admi
}
});
apiRouter.delete('/funnel_stages/:id', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
apiRouter.delete('/funnel_stages/:id', requireRole(['admin', 'super_admin']), async (req, res) => {
try {
await pool.query('DELETE FROM funnel_stages WHERE id = ?', [req.params.id]);
res.json({ message: 'Stage deleted.' });

View File

@@ -239,8 +239,12 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
<>
<SidebarItem to="/admin/users" icon={Users} label="Membros" collapsed={isSidebarCollapsed} />
<SidebarItem to="/admin/teams" icon={Building2} label={currentUser.role === 'manager' ? 'Meu Time' : 'Times'} collapsed={isSidebarCollapsed} />
<SidebarItem to="/admin/funnels" icon={Layers} label="Gerenciar Funis" collapsed={isSidebarCollapsed} />
<SidebarItem to="/admin/origins" icon={Target} label="Origens de Lead" collapsed={isSidebarCollapsed} />
{currentUser.role !== 'manager' && (
<>
<SidebarItem to="/admin/funnels" icon={Layers} label="Gerenciar Funis" collapsed={isSidebarCollapsed} />
<SidebarItem to="/admin/origins" icon={Target} label="Origens de Lead" collapsed={isSidebarCollapsed} />
</>
)}
</>
)}
</>

View File

@@ -1,4 +1,4 @@
version: '3.8'
version: "3.8"
services:
app:
@@ -43,12 +43,31 @@ services:
networks:
- fasto-net
backup-mysql:
image: fradelg/mysql-cron-backup
deploy:
replicas: 1
restart_policy:
condition: on-failure
environment:
MYSQL_HOST: db
MYSQL_USER: root
MYSQL_PASS: ${DB_PASSWORD:-root_password}
CRON_TIME: "55 2 * * *" # Roda todo dia exatamente às 02:55 da manhã
MAX_BACKUPS: 3 # Mantém apenas os 3 últimos dias
INIT_BACKUP: "1" # Faz um backup imediatamente ao ligar o container
volumes:
- /opt/backups_db/fasto:/backup
networks:
- fasto-net
volumes:
db_data:
configs:
init_sql:
file: ./agenciac_comia.sql
name: init_sql_v2
networks:
fasto-net:

View File

@@ -202,9 +202,17 @@ export const UserProfile: React.FC = () => {
id="fullName"
value={name}
onChange={(e) => setName(e.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-zinc-200 dark:border-zinc-800 rounded-lg bg-white dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-600 focus:outline-none focus:ring-2 focus:ring-yellow-400/20 focus:border-yellow-400 transition-all sm:text-sm"
disabled={user.role === 'agent'}
className={`block w-full pl-10 pr-3 py-2 border border-zinc-200 dark:border-zinc-800 rounded-lg sm:text-sm transition-all ${
user.role === 'agent'
? 'bg-zinc-50 dark:bg-zinc-900/50 text-zinc-500 dark:text-zinc-500 cursor-not-allowed'
: 'bg-white dark:bg-zinc-950 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-600 focus:outline-none focus:ring-2 focus:ring-yellow-400/20 focus:border-yellow-400'
}`}
/>
</div>
{user.role === 'agent' && (
<p className="text-xs text-zinc-400 dark:text-zinc-500 mt-1">Contate um administrador para alterar seu nome.</p>
)}
</div>
<div className="space-y-2">