Add in missing permissions, add granular dashboard permissions, fix some issues with user management page
This commit is contained in:
@@ -229,11 +229,14 @@ router.post('/users', authenticate, requirePermission('create:users'), async (re
|
||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// Insert new user
|
||||
// Convert rocket_chat_user_id to integer if provided
|
||||
const rcUserId = rocket_chat_user_id ? parseInt(rocket_chat_user_id, 10) : null;
|
||||
|
||||
const userResult = await client.query(`
|
||||
INSERT INTO users (username, email, password, is_admin, is_active, rocket_chat_user_id, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP)
|
||||
RETURNING id
|
||||
`, [username, email || null, hashedPassword, !!is_admin, is_active !== false, rocket_chat_user_id || null]);
|
||||
`, [username, email || null, hashedPassword, !!is_admin, is_active !== false, rcUserId]);
|
||||
|
||||
const userId = userResult.rows[0].id;
|
||||
|
||||
@@ -360,7 +363,9 @@ router.put('/users/:id', authenticate, requirePermission('edit:users'), async (r
|
||||
|
||||
if (rocket_chat_user_id !== undefined) {
|
||||
updateFields.push(`rocket_chat_user_id = $${paramIndex++}`);
|
||||
updateValues.push(rocket_chat_user_id || null);
|
||||
// Convert to integer if not null/undefined, otherwise null
|
||||
const rcUserId = rocket_chat_user_id ? parseInt(rocket_chat_user_id, 10) : null;
|
||||
updateValues.push(rcUserId);
|
||||
}
|
||||
|
||||
// Update password if provided
|
||||
|
||||
@@ -62,6 +62,12 @@ app.post('/login', async (req, res) => {
|
||||
return res.status(403).json({ error: 'Account is inactive' });
|
||||
}
|
||||
|
||||
// Update last login timestamp
|
||||
await pool.query(
|
||||
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1',
|
||||
[user.id]
|
||||
);
|
||||
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: user.id, username: user.username },
|
||||
|
||||
@@ -154,6 +154,23 @@ Admin users automatically have all permissions.
|
||||
| `settings:templates` | Access to Template Management |
|
||||
| `settings:user_management` | Access to User Management |
|
||||
|
||||
### Dashboard Component Permissions
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| `dashboard:stats` | Can view statistics cards on the Dashboard |
|
||||
| `dashboard:realtime` | Can view realtime analytics on the Dashboard |
|
||||
| `dashboard:financial` | Can view financial overview on the Dashboard |
|
||||
| `dashboard:feed` | Can view event feed on the Dashboard |
|
||||
| `dashboard:sales` | Can view sales chart on the Dashboard |
|
||||
| `dashboard:products` | Can view product grid on the Dashboard |
|
||||
| `dashboard:campaigns` | Can view Klaviyo campaigns on the Dashboard |
|
||||
| `dashboard:analytics` | Can view analytics overview on the Dashboard |
|
||||
| `dashboard:user_behavior` | Can view user behavior insights on the Dashboard |
|
||||
| `dashboard:meta_campaigns` | Can view Meta campaigns on the Dashboard |
|
||||
| `dashboard:typeform` | Can view Typeform metrics on the Dashboard |
|
||||
| `dashboard:gorgias` | Can view Gorgias overview on the Dashboard |
|
||||
| `dashboard:calls` | Can view Aircall metrics on the Dashboard |
|
||||
|
||||
### Admin Permissions
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect, useRef, useContext, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useScroll } from "@/contexts/DashboardScrollContext";
|
||||
import { ArrowUpToLine } from "lucide-react";
|
||||
import { AuthContext } from "@/contexts/AuthContext";
|
||||
|
||||
const Navigation = () => {
|
||||
const [activeSections, setActiveSections] = useState([]);
|
||||
const { isStuck, scrollContainerRef, scrollToSection } = useScroll();
|
||||
const { user } = useContext(AuthContext);
|
||||
const buttonRefs = useRef({});
|
||||
const scrollContainerRef2 = useRef(null);
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
|
||||
const lastScrollLeft = useRef(0);
|
||||
const lastScrollTop = useRef(0);
|
||||
|
||||
// Define base sections that are always visible
|
||||
const baseSections = [
|
||||
{ id: "stats", label: "Statistics" },
|
||||
{ id: "realtime", label: "Realtime" },
|
||||
{ id: "feed", label: "Event Feed" },
|
||||
{ id: "sales", label: "Sales Chart" },
|
||||
{ id: "products", label: "Top Products" },
|
||||
{ id: "campaigns", label: "Campaigns" },
|
||||
{ id: "analytics", label: "Analytics" },
|
||||
{ id: "user-behavior", label: "User Behavior" },
|
||||
{ id: "meta-campaigns", label: "Meta Ads" },
|
||||
{ id: "typeform", label: "Customer Surveys" },
|
||||
{ id: "gorgias-overview", label: "Customer Service" },
|
||||
{ id: "calls", label: "Calls" },
|
||||
// Define all possible sections with their permission requirements
|
||||
const allSections = [
|
||||
{ id: "stats", label: "Statistics", permission: "dashboard:stats" },
|
||||
{ id: "realtime", label: "Realtime", permission: "dashboard:realtime" },
|
||||
{ id: "financial", label: "Financial", permission: "dashboard:financial" },
|
||||
{ id: "feed", label: "Event Feed", permission: "dashboard:feed" },
|
||||
{ id: "sales", label: "Sales Chart", permission: "dashboard:sales" },
|
||||
{ id: "products", label: "Top Products", permission: "dashboard:products" },
|
||||
{ id: "campaigns", label: "Campaigns", permission: "dashboard:campaigns" },
|
||||
{ id: "analytics", label: "Analytics", permission: "dashboard:analytics" },
|
||||
{ id: "user-behavior", label: "User Behavior", permission: "dashboard:user_behavior" },
|
||||
{ id: "meta-campaigns", label: "Meta Ads", permission: "dashboard:meta_campaigns" },
|
||||
{ id: "typeform", label: "Customer Surveys", permission: "dashboard:typeform" },
|
||||
{ id: "gorgias-overview", label: "Customer Service", permission: "dashboard:gorgias" },
|
||||
{ id: "calls", label: "Calls", permission: "dashboard:calls" },
|
||||
];
|
||||
|
||||
// Filter sections based on user permissions
|
||||
const baseSections = useMemo(() => {
|
||||
if (!user) return [];
|
||||
|
||||
// Admins see all sections
|
||||
if (user.is_admin) return allSections;
|
||||
|
||||
// Filter sections based on user permissions
|
||||
return allSections.filter(section =>
|
||||
user.permissions && user.permissions.includes(section.permission)
|
||||
);
|
||||
}, [user]);
|
||||
|
||||
const sortSections = (sections) => {
|
||||
const isMediumScreen = window.matchMedia(
|
||||
"(min-width: 768px) and (max-width: 1023px)"
|
||||
|
||||
@@ -43,7 +43,7 @@ interface User {
|
||||
email?: string;
|
||||
is_admin: boolean;
|
||||
is_active: boolean;
|
||||
rocket_chat_user_id?: string;
|
||||
rocket_chat_user_id?: number;
|
||||
permissions?: Permission[];
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ interface UserSaveData {
|
||||
password?: string;
|
||||
is_admin: boolean;
|
||||
is_active: boolean;
|
||||
rocket_chat_user_id?: string;
|
||||
rocket_chat_user_id?: number;
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
@@ -113,7 +113,8 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
||||
password: "", // Don't pre-fill password
|
||||
is_admin: user?.is_admin || false,
|
||||
is_active: user?.is_active !== false,
|
||||
rocket_chat_user_id: user?.rocket_chat_user_id || "none",
|
||||
// Convert number to string for Select, default to "none"
|
||||
rocket_chat_user_id: user?.rocket_chat_user_id ? user.rocket_chat_user_id.toString() : "none",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -165,7 +166,8 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
||||
password: "", // Don't pre-fill password
|
||||
is_admin: user.is_admin || false,
|
||||
is_active: user.is_active !== false,
|
||||
rocket_chat_user_id: user.rocket_chat_user_id || "none",
|
||||
// Convert number to string for the Select component, or use "none" as default
|
||||
rocket_chat_user_id: user.rocket_chat_user_id ? user.rocket_chat_user_id.toString() : "none",
|
||||
});
|
||||
} else {
|
||||
// For new users, ensure rocket_chat_user_id defaults to "none"
|
||||
@@ -196,7 +198,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
|
||||
const userData: UserSaveData = {
|
||||
...data,
|
||||
id: user?.id, // Include ID if editing existing user
|
||||
rocket_chat_user_id: data.is_admin ? undefined : (data.rocket_chat_user_id === "none" ? undefined : data.rocket_chat_user_id),
|
||||
rocket_chat_user_id: data.is_admin ? undefined : (data.rocket_chat_user_id === "none" || !data.rocket_chat_user_id ? undefined : parseInt(data.rocket_chat_user_id, 10)),
|
||||
permissions: [] // Initialize with empty array
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ScrollProvider } from "@/contexts/DashboardScrollContext";
|
||||
import { ThemeProvider } from "@/components/dashboard/theme/ThemeProvider";
|
||||
import { Protected } from "@/components/auth/Protected";
|
||||
import AircallDashboard from "@/components/dashboard/AircallDashboard";
|
||||
import EventFeed from "@/components/dashboard/EventFeed";
|
||||
import StatCards from "@/components/dashboard/StatCards";
|
||||
@@ -30,60 +31,86 @@ export function Dashboard() {
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-6 gap-4">
|
||||
<div className="xl:col-span-4 col-span-6">
|
||||
<div className="space-y-4 h-full w-full">
|
||||
<div id="stats">
|
||||
<StatCards />
|
||||
<Protected permission="dashboard:stats">
|
||||
<div className="xl:col-span-4 col-span-6">
|
||||
<div className="space-y-4 h-full w-full">
|
||||
<div id="stats">
|
||||
<StatCards />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="realtime" className="xl:col-span-2 col-span-6 overflow-auto">
|
||||
<div className="h-full">
|
||||
<RealtimeAnalytics />
|
||||
</Protected>
|
||||
<Protected permission="dashboard:realtime">
|
||||
<div id="realtime" className="xl:col-span-2 col-span-6 overflow-auto">
|
||||
<div className="h-full">
|
||||
<RealtimeAnalytics />
|
||||
</div>
|
||||
</div>
|
||||
</Protected>
|
||||
</div>
|
||||
<Protected permission="dashboard:financial">
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div id="financial" className="col-span-12">
|
||||
<FinancialOverview />
|
||||
</div>
|
||||
</div>
|
||||
</Protected>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<Protected permission="dashboard:feed">
|
||||
<div id="feed" className="col-span-12 lg:col-span-6 xl:col-span-4 h-[600px] lg:h-[740px]">
|
||||
<EventFeed />
|
||||
</div>
|
||||
</Protected>
|
||||
<Protected permission="dashboard:sales">
|
||||
<div id="sales" className="col-span-12 xl:col-span-8 h-full w-full flex">
|
||||
<SalesChart className="w-full h-full"/>
|
||||
</div>
|
||||
</Protected>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div id="financial" className="col-span-12">
|
||||
<FinancialOverview />
|
||||
</div>
|
||||
<Protected permission="dashboard:products">
|
||||
<div id="products" className="col-span-12 lg:col-span-4 h-[500px]">
|
||||
<ProductGrid />
|
||||
</div>
|
||||
</Protected>
|
||||
<Protected permission="dashboard:campaigns">
|
||||
<div id="campaigns" className="col-span-12 lg:col-span-8">
|
||||
<KlaviyoCampaigns />
|
||||
</div>
|
||||
</Protected>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div id="feed" className="col-span-12 lg:col-span-6 xl:col-span-4 h-[600px] lg:h-[740px]">
|
||||
<EventFeed />
|
||||
<Protected permission="dashboard:analytics">
|
||||
<div id="analytics" className="col-span-12 xl:col-span-8">
|
||||
<AnalyticsDashboard />
|
||||
</div>
|
||||
</Protected>
|
||||
<Protected permission="dashboard:user_behavior">
|
||||
<div id="user-behavior" className="col-span-12 xl:col-span-4">
|
||||
<UserBehaviorDashboard />
|
||||
</div>
|
||||
</Protected>
|
||||
</div>
|
||||
<Protected permission="dashboard:meta_campaigns">
|
||||
<div id="meta-campaigns">
|
||||
<MetaCampaigns />
|
||||
</div>
|
||||
<div id="sales" className="col-span-12 xl:col-span-8 h-full w-full flex">
|
||||
<SalesChart className="w-full h-full"/>
|
||||
</Protected>
|
||||
<Protected permission="dashboard:typeform">
|
||||
<div id="typeform">
|
||||
<TypeformDashboard />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div id="products" className="col-span-12 lg:col-span-4 h-[500px]">
|
||||
<ProductGrid />
|
||||
</Protected>
|
||||
<Protected permission="dashboard:gorgias">
|
||||
<div id="gorgias-overview">
|
||||
<GorgiasOverview />
|
||||
</div>
|
||||
<div id="campaigns" className="col-span-12 lg:col-span-8">
|
||||
<KlaviyoCampaigns />
|
||||
</Protected>
|
||||
<Protected permission="dashboard:calls">
|
||||
<div id="calls">
|
||||
<AircallDashboard />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div id="analytics" className="col-span-12 xl:col-span-8">
|
||||
<AnalyticsDashboard />
|
||||
</div>
|
||||
<div id="user-behavior" className="col-span-12 xl:col-span-4">
|
||||
<UserBehaviorDashboard />
|
||||
</div>
|
||||
</div>
|
||||
<div id="meta-campaigns">
|
||||
<MetaCampaigns />
|
||||
</div>
|
||||
<div id="typeform">
|
||||
<TypeformDashboard />
|
||||
</div>
|
||||
<div id="gorgias-overview">
|
||||
<GorgiasOverview />
|
||||
</div>
|
||||
<div id="calls">
|
||||
<AircallDashboard />
|
||||
</div>
|
||||
</Protected>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user