diff --git a/App.tsx b/App.tsx index 445d4a6..196efb2 100644 --- a/App.tsx +++ b/App.tsx @@ -74,8 +74,8 @@ const App: React.FC = () => { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/GEMINI.md b/GEMINI.md index 14659b8..e5ae2f0 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,33 +1,30 @@ # Fasto Project Documentation ## Overview -Fasto is a commercial team management system built with React (Vite) on the frontend and Node.js (Express) on the backend. It uses a MySQL database. +Fasto is a commercial team management system built with React (Vite) on the frontend and Node.js (Express) on the backend. It uses a MySQL database. It features a complete multi-tenant architecture designed to securely host multiple client organizations within a single deployment. ## 🚀 Recent Major Changes (March 2026) -We have transitioned from a mock-based frontend to a fully functional, production-ready system: +We have transitioned from a mock-based prototype to a **secure, multi-tenant production architecture**: -- **Authentication:** Implemented real JWT-based authentication with password hashing (bcryptjs). -- **Backend Integration:** Replaced all hardcoded constants with real API calls to a Node.js/Express backend connected to a MySQL 8.0 database. -- **RBAC (Role-Based Access Control):** Implemented permissions for `super_admin`, `admin`, `manager`, and `agent`. -- **Membros (Members):** Enhanced to manage roles, teams, and status. Includes a safety modal for deletion. -- **Times (Teams):** Created a new dashboard to manage sales groups with real-time performance metrics. -- **UI/UX:** Standardized PT-BR translations and refined modal layouts. +- **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. +- **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. + - **Agent:** Restricted access. Can only view their own performance metrics and historical attendances. +- **Premium "Onyx & Gold" UI/UX:** Completely redesigned the dark mode using a true neutral Charcoal (Zinc) palette, high-contrast text, and brand Yellow accents. +- **Dynamic KPI Dashboard:** Implemented true period-over-period trend calculations for Leads, Quality Scores, and Response Times. +- **Secure File Uploads:** Profile avatars are now securely uploaded using `multer` with strict mimetype validation (JPG/PNG/WEBP), 2MB size limits, and UUID generation to prevent path traversal. +- **Enhanced Security Flows:** + - User routing uses secure `slugs` instead of exposing raw UUIDs. + - All password reset and setup tokens strictly expire in 15 minutes and are destroyed upon use. + - Separated the "Reset Password" and "Setup Account" (for new admins) flows for better UX. ## 🛠 Architecture - **Frontend**: React 19, TypeScript, Vite, TailwindCSS (CDN). - **Backend**: Node.js, Express, MySQL2 (Pool-based). -- **Database**: MySQL 8.0 (Schema: `agenciac_comia`). +- **Database**: MySQL 8.0 (Schema: `fasto_db`). - **Deployment**: Docker Compose for local development; Gitea Actions for CI/CD pushing to a Gitea Registry and deploying via Portainer webhook. -## ⚠️ Current Error: Build Instability -We are currently resolving a recurring build error: `Unexpected end of file` or `Expected ">" but found "\"`. - -### Technical Root Cause: -This is a **tool-level synchronization issue**: -1. **Truncation:** The file-writing tool (`write_file`) occasionally truncates code before the final braces (`}`) or tags (``) are written. -2. **Escaping Glitches:** In long JSX strings (like Tailwind class lists), the system sometimes inserts accidental characters that break the JavaScript syntax. -3. **Result:** The Vite/Esbuild compiler fails because it reaches the end of an incomplete or syntactically broken file. - ## 📋 Prerequisites - Docker & Docker Compose - Node.js (for local development outside Docker) @@ -39,17 +36,18 @@ Copy `.env.example` to `.env` and adjust values: ```bash cp .env.example .env ``` -Ensure you set the database credentials and `GITEA_RUNNER_REGISTRATION_TOKEN`. +Ensure you set the database credentials (`DB_NAME=fasto_db` for production) and `GITEA_RUNNER_REGISTRATION_TOKEN`. ### 2. Database -The project expects a MySQL database. The `docker-compose.yml` initializes it with `agenciac_comia.sql`. +The project expects a MySQL database. The `docker-compose.local.yml` initializes it with `agenciac_comia.sql`. +*Note for Production:* If migrating from an old version, you must manually run the SQL to create the `password_resets` and `pending_registrations` tables, or rebuild the volume. -### 3. Running with Docker Compose -To start the application, database, and runner: +### 3. Running Locally (Docker Compose) +To start the application and database locally: ```bash -docker-compose up -d --build +docker-compose -f docker-compose.local.yml up -d --build ``` -- **Frontend/Backend**: http://localhost:3001 +- **App**: http://localhost:3001 - **Database**: Port 3306 ### 4. Gitea Runner @@ -64,9 +62,6 @@ The project uses Gitea Actions defined in `.gitea/workflows/build-deploy.yaml`. 2. Build Docker image. 3. Push to `gitea.blyzer.com.br`. 4. Trigger Portainer webhook. -- **Secrets Required in Gitea**: - `REGISTRY_USERNAME`, `REGISTRY_TOKEN`, `PORTAINER_WEBHOOK`, `API_KEY`. ## 💻 Development -- **Frontend**: `npm run dev` (Port 3000) -- **Backend**: `node backend/index.js` (Port 3001) +The Dockerfile uses a unified root structure. Both the frontend build and the backend Node.js server are hosted from the same container image. diff --git a/backend/index.js b/backend/index.js index 385571c..e24e156 100644 --- a/backend/index.js +++ b/backend/index.js @@ -358,7 +358,7 @@ apiRouter.post('/users', requireRole(['admin', 'owner', 'super_admin']), async ( }); apiRouter.put('/users/:id', async (req, res) => { - const { name, bio, role, team_id, status } = req.body; + const { name, bio, role, team_id, status, email } = req.body; try { const [existing] = await pool.query('SELECT * FROM users WHERE id = ?', [req.params.id]); if (existing.length === 0) return res.status(404).json({ error: 'Not found' }); @@ -377,10 +377,16 @@ apiRouter.put('/users/:id', async (req, res) => { const finalRole = isManagerOrAdmin && role !== undefined ? role : existing[0].role; const finalTeamId = isManagerOrAdmin && team_id !== undefined ? team_id : existing[0].team_id; const finalStatus = isManagerOrAdmin && status !== undefined ? status : existing[0].status; + const finalEmail = email !== undefined ? email : existing[0].email; + + if (finalEmail !== existing[0].email) { + const [emailCheck] = await pool.query('SELECT id FROM users WHERE email = ? AND id != ?', [finalEmail, req.params.id]); + if (emailCheck.length > 0) return res.status(400).json({ error: 'E-mail já está em uso.' }); + } await pool.query( - 'UPDATE users SET name = ?, bio = ?, role = ?, team_id = ?, status = ? WHERE id = ?', - [name || existing[0].name, bio !== undefined ? bio : existing[0].bio, finalRole, finalTeamId || null, finalStatus, req.params.id] + 'UPDATE users SET name = ?, bio = ?, email = ?, role = ?, team_id = ?, status = ? WHERE id = ?', + [name || existing[0].name, bio !== undefined ? bio : existing[0].bio, finalEmail, finalRole, finalTeamId || null, finalStatus, req.params.id] ); res.json({ message: 'User updated successfully.' }); } catch (error) { diff --git a/components/Layout.tsx b/components/Layout.tsx index 2a92708..1b249e0 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -112,8 +112,12 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {!isSuperAdmin && ( <> - - + {currentUser.role !== 'agent' && ( + <> + + + + )} )} diff --git a/pages/TeamManagement.tsx b/pages/TeamManagement.tsx index e5ebe47..701dcd6 100644 --- a/pages/TeamManagement.tsx +++ b/pages/TeamManagement.tsx @@ -232,10 +232,19 @@ export const TeamManagement: React.FC = () => {
- setFormData({...formData, role: e.target.value as any})} + className="w-full bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border p-3 rounded-lg text-sm text-zinc-900 dark:text-dark-text disabled:bg-zinc-50 dark:disabled:bg-dark-bg/50 dark:disabled:text-dark-muted" + disabled={ + (currentUser?.role === 'manager') || + (editingUser?.role === 'admin' && currentUser?.role !== 'super_admin') || + (editingUser?.role === 'super_admin' && currentUser?.role !== 'super_admin') + } + > - + {currentUser?.role !== 'manager' && }
diff --git a/pages/UserProfile.tsx b/pages/UserProfile.tsx index 22e320d..3fbf09f 100644 --- a/pages/UserProfile.tsx +++ b/pages/UserProfile.tsx @@ -14,6 +14,7 @@ export const UserProfile: React.FC = () => { const [name, setName] = useState(''); const [bio, setBio] = useState(''); + const [email, setEmail] = useState(''); useEffect(() => { const fetchUserAndTenant = async () => { @@ -25,6 +26,7 @@ export const UserProfile: React.FC = () => { setUser(fetchedUser); setName(fetchedUser.name); setBio(fetchedUser.bio || ''); + setEmail(fetchedUser.email); // Fetch tenant info const tenants = await getTenants(); @@ -85,10 +87,10 @@ export const UserProfile: React.FC = () => { setIsSuccess(false); try { - const success = await updateUser(user.id, { name, bio }); + const success = await updateUser(user.id, { name, bio, email }); if (success) { setIsSuccess(true); - setUser({ ...user, name, bio }); + setUser({ ...user, name, bio, email }); setTimeout(() => setIsSuccess(false), 3000); } else { alert('Erro ao salvar alterações no servidor.'); @@ -107,6 +109,8 @@ export const UserProfile: React.FC = () => { const displayAvatar = user.avatar_url ? (user.avatar_url.startsWith('http') ? user.avatar_url : `${backendUrl}${user.avatar_url}`) : `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=random`; + + const canEditEmail = user.role === 'admin' || user.role === 'super_admin'; return (
@@ -213,12 +217,17 @@ export const UserProfile: React.FC = () => { setEmail(e.target.value)} + disabled={!canEditEmail} + 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 ${ + !canEditEmail + ? '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-brand-yellow/20 focus:border-brand-yellow' + }`} />
-

Contate o admin para alterar o e-mail.

+ {!canEditEmail &&

Contate o admin para alterar o e-mail.

}