From c5edaec7d3edabf1c38fc363710ef2b5ef980d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Fri, 28 Aug 2026 12:49:22 -0300 Subject: [PATCH] Reapply "feat: open administration to all users" This reverts commit 9f4db67f4fb5d66626a4d11c0a95b06fc8f0e60c. --- CONTEXT.md | 2 +- backend/routes/olistRoutes.js | 18 +++++++++--------- backend/routes/userRoutes.js | 10 +++++----- backend/services/userService.js | 3 +-- src/App.tsx | 13 +++---------- src/components/Layout.tsx | 14 ++++++-------- 6 files changed, 25 insertions(+), 35 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 4bcab8c..7b89caa 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -161,7 +161,7 @@ yield: 4.8 units/kg * The production-order screen uses a compact list with inline expansion rather than a side detail page. Each OP has one status control (`Em aberto`, `Em andamento`, `Finalizada`, `Cancelada`), and expanded details show product markers, composition, and material consumption context. Material consumption is an OP-level action, relevant once production has started rather than a permanent page-level form. ### 7.2 Olist V3 connection and composition synchronisation -* Graphs has a Super Admin **Olist** monitor at `/#/admin/olist`. The connection action, manual sync, reconnection, stop action, live status, run history, run logs, and affected-product view are all on this page; Cadastros does not own the Olist connection flow. +* Graphs has an **Administração** section available to every signed-in user. Its **Olist** monitor at `/#/admin/olist` owns the connection action, manual sync, reconnection, stop action, live status, run history, run logs, and affected-product view; Cadastros does not own the Olist connection flow. The same section includes user management at `/#/admin/users`. * OAuth uses Olist/Tiny V3 with an authorization code and refresh token. Tokens are encrypted before storage in `olist_connections`; access tokens are refreshed automatically before expiry. The monitor shows the token expiry as an operational status, not an indication that the connection has failed. * Required production variables are `OLIST_CLIENT_ID`, `OLIST_CLIENT_SECRET`, `OLIST_REDIRECT_URI`, `OLIST_FRONTEND_URL`, `OLIST_TOKEN_ENCRYPTION_KEY`, and `OLIST_SYNC_ENABLED=true`. The encryption key must be stable (a 32-byte base64 key or 64-character hex key): changing it makes already stored tokens unreadable and requires reconnection. * The OAuth callback route is `GET /api/olist/oauth/callback`; `OLIST_REDIRECT_URI` must be exactly that publicly reachable backend URL. After authorization Graphs redirects to `/#/admin/olist` on `OLIST_FRONTEND_URL`. diff --git a/backend/routes/olistRoutes.js b/backend/routes/olistRoutes.js index 6ba67af..bef3e00 100644 --- a/backend/routes/olistRoutes.js +++ b/backend/routes/olistRoutes.js @@ -1,5 +1,5 @@ const express = require('express'); -const { verifySuperAdmin } = require('../auth'); +const { verifyToken } = require('../auth'); const { completeAuthorization, createAuthorizationUrl, @@ -13,7 +13,7 @@ const { exportMissingProductionOrderDataCsv, importFinalizedProductionOrderCsv, const router = express.Router(); -router.get('/olist/status', verifySuperAdmin, async (req, res, next) => { +router.get('/olist/status', verifyToken, async (req, res, next) => { try { res.json(await getOlistStatus({ runsPage: req.query.runsPage, @@ -24,7 +24,7 @@ router.get('/olist/status', verifySuperAdmin, async (req, res, next) => { } }); -router.post('/olist/authorization-url', verifySuperAdmin, async (req, res, next) => { +router.post('/olist/authorization-url', verifyToken, async (req, res, next) => { try { res.json({ url: await createAuthorizationUrl() }); } catch (error) { @@ -32,7 +32,7 @@ router.post('/olist/authorization-url', verifySuperAdmin, async (req, res, next) } }); -router.post('/olist/sync', verifySuperAdmin, async (req, res, next) => { +router.post('/olist/sync', verifyToken, async (req, res, next) => { try { const result = await startOlistSync({ trigger: 'manual', @@ -44,7 +44,7 @@ router.post('/olist/sync', verifySuperAdmin, async (req, res, next) => { } }); -router.post('/olist/sync/stop', verifySuperAdmin, async (req, res, next) => { +router.post('/olist/sync/stop', verifyToken, async (req, res, next) => { try { res.status(202).json(await requestOlistSyncStop()); } catch (error) { @@ -52,7 +52,7 @@ router.post('/olist/sync/stop', verifySuperAdmin, async (req, res, next) => { } }); -router.get('/olist/production-order-imports', verifySuperAdmin, async (req, res, next) => { +router.get('/olist/production-order-imports', verifyToken, async (req, res, next) => { try { res.json(await listProductionOrderImportData({ runsPage: req.query.runsPage, @@ -63,7 +63,7 @@ router.get('/olist/production-order-imports', verifySuperAdmin, async (req, res, } }); -router.get('/olist/production-order-imports/pending-csv', verifySuperAdmin, async (req, res, next) => { +router.get('/olist/production-order-imports/pending-csv', verifyToken, async (req, res, next) => { try { res.json(await exportMissingProductionOrderDataCsv()); } catch (error) { @@ -71,7 +71,7 @@ router.get('/olist/production-order-imports/pending-csv', verifySuperAdmin, asyn } }); -router.post('/olist/production-order-imports', verifySuperAdmin, async (req, res, next) => { +router.post('/olist/production-order-imports', verifyToken, async (req, res, next) => { try { res.status(201).json(await importFinalizedProductionOrderCsv(req.body || {})); } catch (error) { @@ -79,7 +79,7 @@ router.post('/olist/production-order-imports', verifySuperAdmin, async (req, res } }); -router.get('/olist/runs/:runId', verifySuperAdmin, async (req, res, next) => { +router.get('/olist/runs/:runId', verifyToken, async (req, res, next) => { try { res.json(await getOlistRunDetails(req.params.runId, { eventsPage: req.query.eventsPage, diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index dd0bf4c..5269f05 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -1,5 +1,5 @@ const express = require('express'); -const { verifySuperAdmin, verifyToken } = require('../auth'); +const { verifyToken } = require('../auth'); const { createUser, deleteUser, listActiveOperationalUsers, listUsers, updateUser } = require('../services/userService'); const router = express.Router(); @@ -12,7 +12,7 @@ router.get('/users/operational', verifyToken, async (req, res, next) => { } }); -router.get('/users', verifySuperAdmin, async (req, res, next) => { +router.get('/users', verifyToken, async (req, res, next) => { try { const users = await listUsers(); res.json({ users }); @@ -21,7 +21,7 @@ router.get('/users', verifySuperAdmin, async (req, res, next) => { } }); -router.post('/users', verifySuperAdmin, async (req, res, next) => { +router.post('/users', verifyToken, async (req, res, next) => { try { const { name, email, password } = req.body || {}; const { user, password: userPassword, generatedPassword } = await createUser({ name, email, password }); @@ -40,7 +40,7 @@ router.post('/users', verifySuperAdmin, async (req, res, next) => { } }); -router.patch('/users/:id', verifySuperAdmin, async (req, res, next) => { +router.patch('/users/:id', verifyToken, async (req, res, next) => { try { const user = await updateUser(req.params.id, req.body || {}); res.json({ user }); @@ -49,7 +49,7 @@ router.patch('/users/:id', verifySuperAdmin, async (req, res, next) => { } }); -router.delete('/users/:id', verifySuperAdmin, async (req, res, next) => { +router.delete('/users/:id', verifyToken, async (req, res, next) => { try { await deleteUser(req.params.id); res.status(204).send(); diff --git a/backend/services/userService.js b/backend/services/userService.js index 9f7c1a5..84abf6f 100644 --- a/backend/services/userService.js +++ b/backend/services/userService.js @@ -62,8 +62,7 @@ const listUsers = async () => { }; // This deliberately exposes only the identity needed to assign operational -// responsibility. It is available to signed-in Graphs users, unlike the full -// user-management list which remains super-admin only. +// responsibility. It is available to every signed-in Graphs user. const listActiveOperationalUsers = async () => { const result = await pool.query( `SELECT id, name diff --git a/src/App.tsx b/src/App.tsx index 08b5249..d18a6e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import React, { Suspense } from 'react'; import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { Loader2 } from 'lucide-react'; import Layout from './components/Layout'; -import { isAuthenticated, isSuperAdmin } from './dataService'; +import { isAuthenticated } from './dataService'; const Dashboard = React.lazy(() => import('./pages/Dashboard')); const Products = React.lazy(() => import('./pages/Products')); @@ -34,13 +34,6 @@ function PrivateRoute({ children }: { children: React.ReactNode }) { return children; } -function SuperAdminRoute({ children }: { children: React.ReactNode }) { - if (!isSuperAdmin()) { - return ; - } - return children; -} - const RouteFallback = () => (
@@ -75,8 +68,8 @@ function App() { } /> } /> } /> - } /> - } /> + } /> + } /> diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index ba422b0..e0a36a0 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { Outlet, Link, useLocation } from 'react-router-dom'; import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, LogOut, Megaphone, Grid3X3, Shield, Moon, Sun, Tags, Boxes, ClipboardList, Menu, X, BriefcaseBusiness, Radio } from 'lucide-react'; import type { DateRange, OrderData } from '../types'; -import { isSuperAdmin, logout } from '../dataService'; +import { logout } from '../dataService'; import { rangeForLastDays } from '../dateRanges'; const emptyOrdersData: OrderData[] = []; @@ -61,12 +61,10 @@ const Layout = () => { }; const showSidebarLabels = !isSidebarCollapsed || isMobileNavOpen; - const adminNavigation = isSuperAdmin() - ? [ - { name: 'Olist', href: '/admin/olist', icon: Radio }, - { name: 'Usuários', href: '/admin/users', icon: Shield } - ] - : []; + const adminNavigation = [ + { name: 'Olist', href: '/admin/olist', icon: Radio }, + { name: 'Usuários', href: '/admin/users', icon: Shield } + ]; const navigationSections = [ { label: 'Painel', @@ -92,7 +90,7 @@ const Layout = () => { { name: 'Campanhas', href: '/campaigns', icon: Megaphone }, ], }, - ...(adminNavigation.length ? [{ label: 'Super admin', items: adminNavigation }] : []), + { label: 'Administração', items: adminNavigation }, ]; return (