Reapply "feat: open administration to all users"
This reverts commit 9f4db67f4f.
This commit is contained in:
@@ -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.
|
* 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
|
### 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.
|
* 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.
|
* 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`.
|
* 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`.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { verifySuperAdmin } = require('../auth');
|
const { verifyToken } = require('../auth');
|
||||||
const {
|
const {
|
||||||
completeAuthorization,
|
completeAuthorization,
|
||||||
createAuthorizationUrl,
|
createAuthorizationUrl,
|
||||||
@@ -13,7 +13,7 @@ const { exportMissingProductionOrderDataCsv, importFinalizedProductionOrderCsv,
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/olist/status', verifySuperAdmin, async (req, res, next) => {
|
router.get('/olist/status', verifyToken, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
res.json(await getOlistStatus({
|
res.json(await getOlistStatus({
|
||||||
runsPage: req.query.runsPage,
|
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 {
|
try {
|
||||||
res.json({ url: await createAuthorizationUrl() });
|
res.json({ url: await createAuthorizationUrl() });
|
||||||
} catch (error) {
|
} 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 {
|
try {
|
||||||
const result = await startOlistSync({
|
const result = await startOlistSync({
|
||||||
trigger: 'manual',
|
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 {
|
try {
|
||||||
res.status(202).json(await requestOlistSyncStop());
|
res.status(202).json(await requestOlistSyncStop());
|
||||||
} catch (error) {
|
} 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 {
|
try {
|
||||||
res.json(await listProductionOrderImportData({
|
res.json(await listProductionOrderImportData({
|
||||||
runsPage: req.query.runsPage,
|
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 {
|
try {
|
||||||
res.json(await exportMissingProductionOrderDataCsv());
|
res.json(await exportMissingProductionOrderDataCsv());
|
||||||
} catch (error) {
|
} 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 {
|
try {
|
||||||
res.status(201).json(await importFinalizedProductionOrderCsv(req.body || {}));
|
res.status(201).json(await importFinalizedProductionOrderCsv(req.body || {}));
|
||||||
} catch (error) {
|
} 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 {
|
try {
|
||||||
res.json(await getOlistRunDetails(req.params.runId, {
|
res.json(await getOlistRunDetails(req.params.runId, {
|
||||||
eventsPage: req.query.eventsPage,
|
eventsPage: req.query.eventsPage,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { verifySuperAdmin, verifyToken } = require('../auth');
|
const { verifyToken } = require('../auth');
|
||||||
const { createUser, deleteUser, listActiveOperationalUsers, listUsers, updateUser } = require('../services/userService');
|
const { createUser, deleteUser, listActiveOperationalUsers, listUsers, updateUser } = require('../services/userService');
|
||||||
|
|
||||||
const router = express.Router();
|
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 {
|
try {
|
||||||
const users = await listUsers();
|
const users = await listUsers();
|
||||||
res.json({ users });
|
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 {
|
try {
|
||||||
const { name, email, password } = req.body || {};
|
const { name, email, password } = req.body || {};
|
||||||
const { user, password: userPassword, generatedPassword } = await createUser({ name, email, password });
|
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 {
|
try {
|
||||||
const user = await updateUser(req.params.id, req.body || {});
|
const user = await updateUser(req.params.id, req.body || {});
|
||||||
res.json({ user });
|
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 {
|
try {
|
||||||
await deleteUser(req.params.id);
|
await deleteUser(req.params.id);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ const listUsers = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// This deliberately exposes only the identity needed to assign operational
|
// This deliberately exposes only the identity needed to assign operational
|
||||||
// responsibility. It is available to signed-in Graphs users, unlike the full
|
// responsibility. It is available to every signed-in Graphs user.
|
||||||
// user-management list which remains super-admin only.
|
|
||||||
const listActiveOperationalUsers = async () => {
|
const listActiveOperationalUsers = async () => {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`SELECT id, name
|
`SELECT id, name
|
||||||
|
|||||||
13
src/App.tsx
13
src/App.tsx
@@ -2,7 +2,7 @@ import React, { Suspense } from 'react';
|
|||||||
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import Layout from './components/Layout';
|
import Layout from './components/Layout';
|
||||||
import { isAuthenticated, isSuperAdmin } from './dataService';
|
import { isAuthenticated } from './dataService';
|
||||||
|
|
||||||
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
|
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
|
||||||
const Products = React.lazy(() => import('./pages/Products'));
|
const Products = React.lazy(() => import('./pages/Products'));
|
||||||
@@ -34,13 +34,6 @@ function PrivateRoute({ children }: { children: React.ReactNode }) {
|
|||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SuperAdminRoute({ children }: { children: React.ReactNode }) {
|
|
||||||
if (!isSuperAdmin()) {
|
|
||||||
return <Navigate to="/graph" replace />;
|
|
||||||
}
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RouteFallback = () => (
|
const RouteFallback = () => (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-dark-bg text-brand-primary">
|
<div className="flex min-h-screen items-center justify-center bg-dark-bg text-brand-primary">
|
||||||
<Loader2 className="h-8 w-8 animate-spin" />
|
<Loader2 className="h-8 w-8 animate-spin" />
|
||||||
@@ -75,8 +68,8 @@ function App() {
|
|||||||
<Route path="rfm" element={<Rfm />} />
|
<Route path="rfm" element={<Rfm />} />
|
||||||
<Route path="campaigns" element={<Campaigns />} />
|
<Route path="campaigns" element={<Campaigns />} />
|
||||||
<Route path="registrations" element={<Registrations />} />
|
<Route path="registrations" element={<Registrations />} />
|
||||||
<Route path="admin/users" element={<SuperAdminRoute><AdminUsers /></SuperAdminRoute>} />
|
<Route path="admin/users" element={<AdminUsers />} />
|
||||||
<Route path="admin/olist" element={<SuperAdminRoute><OlistSync /></SuperAdminRoute>} />
|
<Route path="admin/olist" element={<OlistSync />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Outlet, Link, useLocation } from 'react-router-dom';
|
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 { 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 type { DateRange, OrderData } from '../types';
|
||||||
import { isSuperAdmin, logout } from '../dataService';
|
import { logout } from '../dataService';
|
||||||
import { rangeForLastDays } from '../dateRanges';
|
import { rangeForLastDays } from '../dateRanges';
|
||||||
|
|
||||||
const emptyOrdersData: OrderData[] = [];
|
const emptyOrdersData: OrderData[] = [];
|
||||||
@@ -61,12 +61,10 @@ const Layout = () => {
|
|||||||
};
|
};
|
||||||
const showSidebarLabels = !isSidebarCollapsed || isMobileNavOpen;
|
const showSidebarLabels = !isSidebarCollapsed || isMobileNavOpen;
|
||||||
|
|
||||||
const adminNavigation = isSuperAdmin()
|
const adminNavigation = [
|
||||||
? [
|
|
||||||
{ name: 'Olist', href: '/admin/olist', icon: Radio },
|
{ name: 'Olist', href: '/admin/olist', icon: Radio },
|
||||||
{ name: 'Usuários', href: '/admin/users', icon: Shield }
|
{ name: 'Usuários', href: '/admin/users', icon: Shield }
|
||||||
]
|
];
|
||||||
: [];
|
|
||||||
const navigationSections = [
|
const navigationSections = [
|
||||||
{
|
{
|
||||||
label: 'Painel',
|
label: 'Painel',
|
||||||
@@ -92,7 +90,7 @@ const Layout = () => {
|
|||||||
{ name: 'Campanhas', href: '/campaigns', icon: Megaphone },
|
{ name: 'Campanhas', href: '/campaigns', icon: Megaphone },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...(adminNavigation.length ? [{ label: 'Super admin', items: adminNavigation }] : []),
|
{ label: 'Administração', items: adminNavigation },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user