chore: add automated database backup service and tighten backend security
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m56s
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.
This commit is contained in:
162
App.tsx
162
App.tsx
@@ -1,34 +1,48 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { HashRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
import {
|
||||||
import { Layout } from './components/Layout';
|
HashRouter as Router,
|
||||||
import { Dashboard } from './pages/Dashboard';
|
Routes,
|
||||||
import { UserDetail } from './pages/UserDetail';
|
Route,
|
||||||
import { AttendanceDetail } from './pages/AttendanceDetail';
|
Navigate,
|
||||||
import { SuperAdmin } from './pages/SuperAdmin';
|
useLocation,
|
||||||
import { ApiKeys } from './pages/ApiKeys';
|
} from "react-router-dom";
|
||||||
import { TeamManagement } from './pages/TeamManagement';
|
import { Layout } from "./components/Layout";
|
||||||
import { Teams } from './pages/Teams';
|
import { Dashboard } from "./pages/Dashboard";
|
||||||
import { Funnels } from './pages/Funnels';
|
import { UserDetail } from "./pages/UserDetail";
|
||||||
import { Origins } from './pages/Origins';
|
import { AttendanceDetail } from "./pages/AttendanceDetail";
|
||||||
import { Login } from './pages/Login';
|
import { SuperAdmin } from "./pages/SuperAdmin";
|
||||||
import { ForgotPassword } from './pages/ForgotPassword';
|
import { ApiKeys } from "./pages/ApiKeys";
|
||||||
import { ResetPassword } from './pages/ResetPassword';
|
import { TeamManagement } from "./pages/TeamManagement";
|
||||||
import { SetupAccount } from './pages/SetupAccount';
|
import { Teams } from "./pages/Teams";
|
||||||
import { UserProfile } from './pages/UserProfile';
|
import { Funnels } from "./pages/Funnels";
|
||||||
import { getUserById, logout } from './services/dataService';
|
import { Origins } from "./pages/Origins";
|
||||||
import { User } from './types';
|
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 [user, setUser] = useState<User | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
const storedUserId = localStorage.getItem('ctms_user_id');
|
const storedUserId = localStorage.getItem("ctms_user_id");
|
||||||
const storedToken = localStorage.getItem('ctms_token');
|
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
|
if (storedToken) logout(); // Limpar se for "undefined" string
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -37,7 +51,7 @@ const AuthGuard: React.FC<{ children: React.ReactNode, roles?: string[] }> = ({
|
|||||||
try {
|
try {
|
||||||
const fetchedUser = await getUserById(storedUserId);
|
const fetchedUser = await getUserById(storedUserId);
|
||||||
if (fetchedUser) {
|
if (fetchedUser) {
|
||||||
if (fetchedUser.status === 'active') {
|
if (fetchedUser.status === "active") {
|
||||||
setUser(fetchedUser);
|
setUser(fetchedUser);
|
||||||
} else {
|
} else {
|
||||||
// User explicitly marked inactive or deleted
|
// User explicitly marked inactive or deleted
|
||||||
@@ -53,7 +67,7 @@ const AuthGuard: React.FC<{ children: React.ReactNode, roles?: string[] }> = ({
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Auth check failed (network/server error):", 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 shouldn't wipe the user's local storage tokens.
|
||||||
// We just leave the user as null, which will redirect them to login,
|
// 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.
|
// 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]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
if (loading) {
|
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) {
|
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
|
// 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 />;
|
return <Navigate to="/super-admin" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,16 +111,86 @@ const App: React.FC = () => {
|
|||||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||||
<Route path="/reset-password" element={<ResetPassword />} />
|
<Route path="/reset-password" element={<ResetPassword />} />
|
||||||
<Route path="/setup-account" element={<SetupAccount />} />
|
<Route path="/setup-account" element={<SetupAccount />} />
|
||||||
<Route path="/" element={<AuthGuard><Dashboard /></AuthGuard>} />
|
<Route
|
||||||
<Route path="/admin/users" element={<AuthGuard roles={['super_admin', 'admin', 'manager']}><TeamManagement /></AuthGuard>} />
|
path="/"
|
||||||
<Route path="/admin/teams" element={<AuthGuard roles={['super_admin', 'admin', 'manager']}><Teams /></AuthGuard>} />
|
element={
|
||||||
<Route path="/admin/funnels" element={<AuthGuard roles={['super_admin', 'admin', 'manager']}><Funnels /></AuthGuard>} />
|
<AuthGuard>
|
||||||
<Route path="/admin/origins" element={<AuthGuard roles={['super_admin', 'admin', 'manager']}><Origins /></AuthGuard>} />
|
<Dashboard />
|
||||||
<Route path="/users/:id" element={<AuthGuard><UserDetail /></AuthGuard>} />
|
</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
|
||||||
<Route path="/profile" element={<AuthGuard><UserProfile /></AuthGuard>} />
|
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 />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
50
GEMINI.md
50
GEMINI.md
@@ -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**:
|
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.
|
- **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.
|
- **Advanced 2-Token Authentication (Rolling Sessions):**
|
||||||
- **Dynamic Funnel Customization:**
|
- Replaced the vulnerable 1-year static JWT with a highly secure dual-token system.
|
||||||
- Funnel stages are no longer hardcoded ENUMs. Each tenant can create multiple dynamic funnels via the `funnels` and `funnel_stages` tables.
|
- Generates a short-lived `AccessToken` (15 min) and a stateful `RefreshToken` (30 days) stored in the DB (`refresh_tokens` table).
|
||||||
- Managers can assign specific Teams to specific Funnels.
|
- 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.
|
||||||
- The Dashboard, User Detail, and Attendance Detail pages now dynamically map and color-code stages based on the active team's assigned funnel.
|
- Full remote revocation capability (Logout drops the token from the DB immediately).
|
||||||
- **Real-Time Notification System:**
|
- **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.
|
||||||
- Built a persistent notification tray (`/api/notifications`) with real-time polling (10s intervals) and a hidden HTML5 `<audio>` player for cross-browser sound playback.
|
- **Role-Based Access Control (RBAC) Simplification:**
|
||||||
- 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.
|
- Removed the redundant 'owner' role. The system now strictly relies on 4 tiers:
|
||||||
- **Role-Based Access Control (RBAC):**
|
- **Super Admin:** Global management of all tenants and API keys (via the hidden `system` tenant).
|
||||||
- **Super Admin:** Global management of all tenants and users (via the hidden `system` tenant).
|
- **Admin:** Full control over members, teams, funnels, and origins within their specific organization.
|
||||||
- **Admin/Manager:** Full control over members and teams 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.
|
- **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:**
|
- **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`).
|
- Fully collapsible interactive sidebar with memory (`localStorage`).
|
||||||
- Action buttons across all data tables are permanently visible for faster discoverability (removed hover requirements).
|
- All Date/Time displays localized to strict Brazilian formatting (`pt-BR`, 24h, `DD/MM/YY`).
|
||||||
- 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.
|
|
||||||
|
|
||||||
## 📌 Roadmap / To-Do
|
## 📌 Roadmap / To-Do
|
||||||
- [ ] **n8n / External API Integration (Priority 1):**
|
- [ ] **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`).
|
||||||
- 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.
|
|
||||||
- [ ] **Data Export/Reporting:** Allow Admins to export attendance and KPI data to CSV/Excel.
|
- [ ] **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.
|
- [ ] **Billing/Subscription Management:** Integrate a payment gateway (e.g., Stripe/Asaas) to manage tenant trial periods and active statuses dynamically.
|
||||||
|
|
||||||
## 🛠 Architecture
|
## 🛠 Architecture
|
||||||
- **Frontend**: React 19, TypeScript, Vite, TailwindCSS (CDN), Recharts, Lucide React.
|
- **Frontend**: React 19, TypeScript, Vite, TailwindCSS (CDN), Recharts, Lucide React.
|
||||||
- **Backend**: Node.js, Express, MySQL2 (Pool-based), Nodemailer.
|
- **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.
|
- **Deployment**: Docker Compose for local development; Gitea Actions for CI/CD pushing to a Gitea Registry and deploying via Portainer webhook.
|
||||||
|
|
||||||
## 📋 Prerequisites
|
## 📋 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.
|
*Note:* The backend automatically strips literal quotes from Docker `.env` string values (like `SMTP_PASS`) to prevent authentication crashes.
|
||||||
|
|
||||||
### 2. Database
|
### 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)
|
### 3. Running Locally (Docker Compose)
|
||||||
To start the application and database locally:
|
To start the application and database locally:
|
||||||
|
|||||||
@@ -136,14 +136,17 @@ const authenticateToken = async (req, res, next) => {
|
|||||||
|
|
||||||
if (!token) return res.status(401).json({ error: 'Token não fornecido.' });
|
if (!token) return res.status(401).json({ error: 'Token não fornecido.' });
|
||||||
|
|
||||||
jwt.verify(token, JWT_SECRET, (err, user) => {
|
try {
|
||||||
if (err) return res.status(401).json({ error: 'Token inválido ou expirado.' });
|
const user = jwt.verify(token, JWT_SECRET);
|
||||||
req.user = user;
|
req.user = user;
|
||||||
next();
|
next();
|
||||||
});
|
} catch (err) {
|
||||||
|
return res.status(401).json({ error: 'Token inválido ou expirado.' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const requireRole = (roles) => (req, res, next) => {
|
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.' });
|
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();
|
next();
|
||||||
};
|
};
|
||||||
@@ -457,7 +460,7 @@ apiRouter.get('/users/:idOrSlug', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM users WHERE id = ? OR slug = ?', [req.params.idOrSlug, req.params.idOrSlug]);
|
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 (!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) {
|
if (req.user.role !== 'super_admin' && rows[0].tenant_id !== req.user.tenant_id) {
|
||||||
return res.status(403).json({ error: 'Acesso negado.' });
|
return res.status(403).json({ error: 'Acesso negado.' });
|
||||||
|
|||||||
@@ -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/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/teams" icon={Building2} label={currentUser.role === 'manager' ? 'Meu Time' : 'Times'} collapsed={isSidebarCollapsed} />
|
||||||
<SidebarItem to="/admin/funnels" icon={Layers} label="Gerenciar Funis" collapsed={isSidebarCollapsed} />
|
{currentUser.role !== 'manager' && (
|
||||||
<SidebarItem to="/admin/origins" icon={Target} label="Origens de Lead" 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} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: '3.8'
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
@@ -43,6 +43,23 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- fasto-net
|
- fasto-net
|
||||||
|
|
||||||
|
backup-mysql:
|
||||||
|
image: databack/mysql-backup
|
||||||
|
environment:
|
||||||
|
DB_SERVER: db
|
||||||
|
DB_USER: root
|
||||||
|
DB_PASS: ${DB_PASSWORD:-root_password}
|
||||||
|
DB_DUMP_CRON: "55 2 * * *" # Roda todo dia exatamente às 02:55 da manhã
|
||||||
|
DB_CLEANUP_TIME: 4320 # Apaga os locais mais velhos que 3 dias
|
||||||
|
volumes:
|
||||||
|
- /root/backups_db:/db
|
||||||
|
networks:
|
||||||
|
- fasto-net
|
||||||
|
deploy:
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.role == manager
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_data:
|
db_data:
|
||||||
|
|
||||||
|
|||||||
@@ -202,9 +202,17 @@ export const UserProfile: React.FC = () => {
|
|||||||
id="fullName"
|
id="fullName"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
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>
|
</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>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user