Add in missing permissions, add granular dashboard permissions, fix some issues with user management page

This commit is contained in:
2025-09-30 22:21:02 -04:00
parent ff17b290aa
commit c6e4fc9cff
6 changed files with 138 additions and 65 deletions

View File

@@ -229,11 +229,14 @@ router.post('/users', authenticate, requirePermission('create:users'), async (re
const hashedPassword = await bcrypt.hash(password, saltRounds); const hashedPassword = await bcrypt.hash(password, saltRounds);
// Insert new user // 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(` const userResult = await client.query(`
INSERT INTO users (username, email, password, is_admin, is_active, rocket_chat_user_id, created_at) 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) VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP)
RETURNING id 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; 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) { if (rocket_chat_user_id !== undefined) {
updateFields.push(`rocket_chat_user_id = $${paramIndex++}`); 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 // Update password if provided

View File

@@ -62,6 +62,12 @@ app.post('/login', async (req, res) => {
return res.status(403).json({ error: 'Account is inactive' }); 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 // Generate JWT token
const token = jwt.sign( const token = jwt.sign(
{ userId: user.id, username: user.username }, { userId: user.id, username: user.username },

View File

@@ -154,6 +154,23 @@ Admin users automatically have all permissions.
| `settings:templates` | Access to Template Management | | `settings:templates` | Access to Template Management |
| `settings:user_management` | Access to User 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 ### Admin Permissions
| Code | Description | | Code | Description |
|------|-------------| |------|-------------|

View File

@@ -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 { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useScroll } from "@/contexts/DashboardScrollContext"; import { useScroll } from "@/contexts/DashboardScrollContext";
import { ArrowUpToLine } from "lucide-react"; import { ArrowUpToLine } from "lucide-react";
import { AuthContext } from "@/contexts/AuthContext";
const Navigation = () => { const Navigation = () => {
const [activeSections, setActiveSections] = useState([]); const [activeSections, setActiveSections] = useState([]);
const { isStuck, scrollContainerRef, scrollToSection } = useScroll(); const { isStuck, scrollContainerRef, scrollToSection } = useScroll();
const { user } = useContext(AuthContext);
const buttonRefs = useRef({}); const buttonRefs = useRef({});
const scrollContainerRef2 = useRef(null); const scrollContainerRef2 = useRef(null);
const [shouldAutoScroll, setShouldAutoScroll] = useState(true); const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
const lastScrollLeft = useRef(0); const lastScrollLeft = useRef(0);
const lastScrollTop = useRef(0); const lastScrollTop = useRef(0);
// Define base sections that are always visible // Define all possible sections with their permission requirements
const baseSections = [ const allSections = [
{ id: "stats", label: "Statistics" }, { id: "stats", label: "Statistics", permission: "dashboard:stats" },
{ id: "realtime", label: "Realtime" }, { id: "realtime", label: "Realtime", permission: "dashboard:realtime" },
{ id: "feed", label: "Event Feed" }, { id: "financial", label: "Financial", permission: "dashboard:financial" },
{ id: "sales", label: "Sales Chart" }, { id: "feed", label: "Event Feed", permission: "dashboard:feed" },
{ id: "products", label: "Top Products" }, { id: "sales", label: "Sales Chart", permission: "dashboard:sales" },
{ id: "campaigns", label: "Campaigns" }, { id: "products", label: "Top Products", permission: "dashboard:products" },
{ id: "analytics", label: "Analytics" }, { id: "campaigns", label: "Campaigns", permission: "dashboard:campaigns" },
{ id: "user-behavior", label: "User Behavior" }, { id: "analytics", label: "Analytics", permission: "dashboard:analytics" },
{ id: "meta-campaigns", label: "Meta Ads" }, { id: "user-behavior", label: "User Behavior", permission: "dashboard:user_behavior" },
{ id: "typeform", label: "Customer Surveys" }, { id: "meta-campaigns", label: "Meta Ads", permission: "dashboard:meta_campaigns" },
{ id: "gorgias-overview", label: "Customer Service" }, { id: "typeform", label: "Customer Surveys", permission: "dashboard:typeform" },
{ id: "calls", label: "Calls" }, { 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 sortSections = (sections) => {
const isMediumScreen = window.matchMedia( const isMediumScreen = window.matchMedia(
"(min-width: 768px) and (max-width: 1023px)" "(min-width: 768px) and (max-width: 1023px)"

View File

@@ -43,7 +43,7 @@ interface User {
email?: string; email?: string;
is_admin: boolean; is_admin: boolean;
is_active: boolean; is_active: boolean;
rocket_chat_user_id?: string; rocket_chat_user_id?: number;
permissions?: Permission[]; permissions?: Permission[];
} }
@@ -94,7 +94,7 @@ interface UserSaveData {
password?: string; password?: string;
is_admin: boolean; is_admin: boolean;
is_active: boolean; is_active: boolean;
rocket_chat_user_id?: string; rocket_chat_user_id?: number;
permissions: Permission[]; permissions: Permission[];
} }
@@ -113,7 +113,8 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
password: "", // Don't pre-fill password password: "", // Don't pre-fill password
is_admin: user?.is_admin || false, is_admin: user?.is_admin || false,
is_active: user?.is_active !== 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 password: "", // Don't pre-fill password
is_admin: user.is_admin || false, is_admin: user.is_admin || false,
is_active: user.is_active !== 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 { } else {
// For new users, ensure rocket_chat_user_id defaults to "none" // 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 = { const userData: UserSaveData = {
...data, ...data,
id: user?.id, // Include ID if editing existing user 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 permissions: [] // Initialize with empty array
}; };

View File

@@ -1,5 +1,6 @@
import { ScrollProvider } from "@/contexts/DashboardScrollContext"; import { ScrollProvider } from "@/contexts/DashboardScrollContext";
import { ThemeProvider } from "@/components/dashboard/theme/ThemeProvider"; import { ThemeProvider } from "@/components/dashboard/theme/ThemeProvider";
import { Protected } from "@/components/auth/Protected";
import AircallDashboard from "@/components/dashboard/AircallDashboard"; import AircallDashboard from "@/components/dashboard/AircallDashboard";
import EventFeed from "@/components/dashboard/EventFeed"; import EventFeed from "@/components/dashboard/EventFeed";
import StatCards from "@/components/dashboard/StatCards"; import StatCards from "@/components/dashboard/StatCards";
@@ -30,60 +31,86 @@ export function Dashboard() {
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div className="grid grid-cols-1 xl:grid-cols-6 gap-4"> <div className="grid grid-cols-1 xl:grid-cols-6 gap-4">
<div className="xl:col-span-4 col-span-6"> <Protected permission="dashboard:stats">
<div className="space-y-4 h-full w-full"> <div className="xl:col-span-4 col-span-6">
<div id="stats"> <div className="space-y-4 h-full w-full">
<StatCards /> <div id="stats">
<StatCards />
</div>
</div> </div>
</div> </div>
</div> </Protected>
<div id="realtime" className="xl:col-span-2 col-span-6 overflow-auto"> <Protected permission="dashboard:realtime">
<div className="h-full"> <div id="realtime" className="xl:col-span-2 col-span-6 overflow-auto">
<RealtimeAnalytics /> <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>
</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>
<div className="grid grid-cols-12 gap-4"> <div className="grid grid-cols-12 gap-4">
<div id="financial" className="col-span-12"> <Protected permission="dashboard:products">
<FinancialOverview /> <div id="products" className="col-span-12 lg:col-span-4 h-[500px]">
</div> <ProductGrid />
</div>
</Protected>
<Protected permission="dashboard:campaigns">
<div id="campaigns" className="col-span-12 lg:col-span-8">
<KlaviyoCampaigns />
</div>
</Protected>
</div> </div>
<div className="grid grid-cols-12 gap-4"> <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]"> <Protected permission="dashboard:analytics">
<EventFeed /> <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>
<div id="sales" className="col-span-12 xl:col-span-8 h-full w-full flex"> </Protected>
<SalesChart className="w-full h-full"/> <Protected permission="dashboard:typeform">
<div id="typeform">
<TypeformDashboard />
</div> </div>
</div> </Protected>
<div className="grid grid-cols-12 gap-4"> <Protected permission="dashboard:gorgias">
<div id="products" className="col-span-12 lg:col-span-4 h-[500px]"> <div id="gorgias-overview">
<ProductGrid /> <GorgiasOverview />
</div> </div>
<div id="campaigns" className="col-span-12 lg:col-span-8"> </Protected>
<KlaviyoCampaigns /> <Protected permission="dashboard:calls">
<div id="calls">
<AircallDashboard />
</div> </div>
</div> </Protected>
<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>
</div> </div>
</div> </div>
</div> </div>