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);
|
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
|
||||||
|
|||||||
@@ -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 },
|
||||||
@@ -76,7 +82,7 @@ app.post('/login', async (req, res) => {
|
|||||||
JOIN user_permissions up ON p.id = up.permission_id
|
JOIN user_permissions up ON p.id = up.permission_id
|
||||||
WHERE up.user_id = $1
|
WHERE up.user_id = $1
|
||||||
`, [user.id]);
|
`, [user.id]);
|
||||||
|
|
||||||
const permissions = permissionsResult.rows.map(row => row.code);
|
const permissions = permissionsResult.rows.map(row => row.code);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -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 |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user