Skip to content

Commit 8392a38

Browse files
Zeff01claude
andcommitted
feat: admin controls, database migrations, and security fixes
## Features - Add Admin Controls page for News Banners, Surveys, and Services - Remove Roles and Permissions from Settings page - Update sidebar navigation with Admin Controls link ## Database Migrations - Schema fixes: Add missing FKs, remove unused tables/columns - RLS critical fixes: Tighten security policies for profile_points, attendance_points - RLS duplicate removal: Optimize policies for better performance - RLS hotfix: Restore functionality for attendance, kanban, overflow, feeds - Critical security fixes: Fix attendance_points, overflow likes, add helper functions - Drop obsolete social_points RPC function ## Bug Fixes - Fix Tiptap extension versions (downgrade color and text-style to v2.27.1) - Fix admin_users view to include all admin users correctly - Remove unused SVG icons (icon-key, icon-roles) ## Configuration - Update middleware route protection for Admin Controls - Update paths config to remove roles/permissions routes - Update settings constants to move admin features 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent be51e6c commit 8392a38

21 files changed

Lines changed: 2751 additions & 272 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Box from "@/components/shared/dashboard/Box";
2+
import { Skeleton } from "@/components/ui/skeleton/skeleton";
3+
4+
const Loading = () => {
5+
return (
6+
<div className="mx-auto flex max-w-screen-xl flex-col gap-4">
7+
<Skeleton className="h-8 w-40 rounded-lg" />
8+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
9+
{Array(3)
10+
.fill(null)
11+
.map((_, index) => (
12+
<Box
13+
key={index}
14+
className="flex gap-2 lg:h-72 lg:flex-col lg:gap-3"
15+
>
16+
<Skeleton className="h-auto w-1/2 rounded-lg lg:h-1/2 lg:w-full" />
17+
<div className="flex w-full flex-col gap-2">
18+
<Skeleton className="h-8 w-1/2 rounded-lg" />
19+
<Skeleton className="h-8 rounded-lg" />
20+
<Skeleton className="hidden h-8 rounded-lg lg:block" />
21+
</div>
22+
</Box>
23+
))}
24+
</div>
25+
</div>
26+
);
27+
};
28+
29+
export default Loading;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Link from "next/link";
2+
import SettingsCard from "@/app/home/settings/_components/SettingsCard";
3+
import { H1 } from "@/components/shared/dashboard";
4+
import { adminControlsCardData } from "@/constants/settings";
5+
import PageContainer from "../_components/PageContainer";
6+
7+
const AdminControls = () => {
8+
return (
9+
<PageContainer maxWidth="xl">
10+
<H1>Admin Controls</H1>
11+
<div className="mt-4 grid w-full grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
12+
{adminControlsCardData.map((card) => (
13+
<Link key={card.path} href={card.path}>
14+
<SettingsCard
15+
imageName={card.imageName}
16+
imageAlt={card.imageAlt}
17+
title={card.title}
18+
description={card.description}
19+
/>
20+
</Link>
21+
))}
22+
</div>
23+
</PageContainer>
24+
);
25+
};
26+
27+
export default AdminControls;
Lines changed: 2 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,171 +1,15 @@
1-
"use client";
2-
3-
import { useEffect, useState } from "react";
41
import Link from "next/link";
52
import SettingsCard from "@/app/home/settings/_components/SettingsCard";
63
import { H1 } from "@/components/shared/dashboard";
74
import { settingsCardData } from "@/constants/settings";
8-
import { createClientClientComponent } from "@/utils/supabase/client";
95
import PageContainer from "../_components/PageContainer";
106

11-
import Loading from "./loading";
12-
13-
type RolePermissions = {
14-
dashboard: boolean;
15-
kanban: boolean;
16-
time_tracker: boolean;
17-
interns: boolean;
18-
applicants: boolean;
19-
inhouse: boolean;
20-
clients: boolean;
21-
projects: boolean;
22-
settings: boolean;
23-
orgchart: boolean;
24-
resume?: boolean;
25-
permissions?: boolean;
26-
roles?: boolean;
27-
};
28-
29-
type PermissionKey = keyof RolePermissions;
30-
31-
// Admin-only cards - only visible to Admin role (role_id = 1)
32-
// Hidden from: Codev (10), Intern (4), HR (2), and all other non-admin roles
33-
const ADMIN_ONLY_PATHS = [
34-
"/home/settings/news-banners",
35-
"/home/settings/surveys",
36-
"/home/settings/services",
37-
];
38-
39-
// Map specific settings routes to their respective permission keys
40-
const settingsPermissionMap: Record<string, PermissionKey> = {
41-
"/home/settings/profile": "resume",
42-
"/home/settings/permissions": "permissions",
43-
"/home/settings/roles": "roles",
44-
};
45-
467
const Settings = () => {
47-
const [supabase, setSupabase] = useState<any>(null);
48-
const [roleId, setRoleId] = useState<number | null>(null);
49-
const [rolePermissions, setRolePermissions] = useState<RolePermissions | null>(null);
50-
const [loading, setLoading] = useState<boolean>(true);
51-
52-
// Initialize Supabase client
53-
useEffect(() => {
54-
const client = createClientClientComponent();
55-
setSupabase(client);
56-
}, []);
57-
58-
// Get current user's role_id by mapping auth email to codev.role_id
59-
useEffect(() => {
60-
if (!supabase) return;
61-
62-
async function fetchUserRole() {
63-
try {
64-
// Get auth user
65-
const { data: { session }, error: sessionError } = await supabase.auth.getSession();
66-
67-
if (sessionError || !session?.user?.email) {
68-
setLoading(false);
69-
return;
70-
}
71-
72-
// Map email to codev profile to get role_id
73-
const { data: codevData, error: codevError } = await supabase
74-
.from("codev")
75-
.select("role_id, first_name, last_name")
76-
.eq("email_address", session.user.email)
77-
.single();
78-
79-
if (codevError || !codevData) {
80-
setLoading(false);
81-
return;
82-
}
83-
84-
setRoleId(codevData.role_id);
85-
} catch (err) {
86-
setLoading(false);
87-
}
88-
}
89-
90-
fetchUserRole();
91-
}, [supabase]);
92-
93-
// Fetch role permissions once we have roleId
94-
useEffect(() => {
95-
if (!supabase || roleId === null) return;
96-
97-
async function fetchRolePermissions() {
98-
try {
99-
const { data: roleData, error: roleError } = await supabase
100-
.from("roles")
101-
.select(
102-
"id, name, dashboard, kanban, time_tracker, interns, applicants, inhouse, clients, projects, settings, orgchart, resume, permissions, roles"
103-
)
104-
.eq("id", roleId)
105-
.single();
106-
107-
if (roleError || !roleData) {
108-
setLoading(false);
109-
return;
110-
}
111-
112-
setRolePermissions(roleData);
113-
} catch (err) {
114-
// Error fetching role permissions
115-
} finally {
116-
setLoading(false);
117-
}
118-
}
119-
120-
fetchRolePermissions();
121-
}, [supabase, roleId]);
122-
123-
// Helper function to check a permission
124-
const hasPermission = (permission: PermissionKey): boolean => {
125-
return !!rolePermissions?.[permission];
126-
};
127-
128-
if (loading) {
129-
return (
130-
<PageContainer>
131-
<Loading />
132-
</PageContainer>
133-
);
134-
}
135-
136-
if (!rolePermissions || roleId === null) {
137-
return (
138-
<PageContainer>
139-
<div className="mx-auto p-4">Unable to determine permissions.</div>
140-
</PageContainer>
141-
);
142-
}
143-
144-
// Filter settings cards based on role
145-
const filteredCards = settingsCardData.filter((card) => {
146-
// Rule 1: Admin-only cards (News Banners, Surveys, Services)
147-
// ONLY visible if user has role_id = 1 (Admin)
148-
// BLOCKED for: Codev (10), Intern (4), HR (2), all others
149-
if (ADMIN_ONLY_PATHS.includes(card.path)) {
150-
return roleId === 1;
151-
}
152-
153-
// Rule 2: Check specific permission mapping
154-
// For cards like Profile, Permissions, Roles
155-
const permissionKey = settingsPermissionMap[card.path];
156-
if (permissionKey) {
157-
return hasPermission(permissionKey);
158-
}
159-
160-
// Rule 3: Default fallback - require general "settings" permission
161-
return hasPermission("settings");
162-
});
163-
1648
return (
1659
<PageContainer maxWidth="xl">
16610
<H1>Settings</H1>
16711
<div className="mt-4 grid w-full grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
168-
{filteredCards.map((card) => (
12+
{settingsCardData.map((card) => (
16913
<Link key={card.path} href={card.path}>
17014
<SettingsCard
17115
imageName={card.imageName}
@@ -180,4 +24,4 @@ const Settings = () => {
18024
);
18125
};
18226

183-
export default Settings;
27+
export default Settings;

‎apps/codebility/config/paths.config.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ const PathsSchema = z.object({
2121
tasks: z.string().min(1),
2222
time_tracker: z.string().min(1),
2323
settings: z.string().min(1),
24-
roles: z.string().min(1),
24+
admin_controls: z.string().min(1),
2525
services: z.string().min(1),
26-
permissions: z.string().min(1),
2726
resume: z.string().min(1),
2827
my_team: z.string().min(1),
2928
admin_dashboard: z.string().min(1),
@@ -53,9 +52,8 @@ const pathsConfig = PathsSchema.parse({
5352
tasks: "/home/tasks",
5453
time_tracker: "/home/time-tracker",
5554
settings: "/home/settings",
56-
roles: "/home/settings/roles",
55+
admin_controls: "/home/admin-controls",
5756
services: "/home/settings/services",
58-
permissions: "/home/settings/permissions",
5957
resume: "/home/settings/resume",
6058
admin_dashboard: "/home/admin-dashboard",
6159
overflow: "/home/overflow",

‎apps/codebility/constants/settings.ts‎

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,15 @@ type SettingsCardDataProps = {
88

99
export const settingsCardData: SettingsCardDataProps[] = [
1010
{
11-
path: "/home/settings/roles",
12-
imageName: "icon-roles",
13-
imageAlt: "Icon Roles",
14-
title: "Roles",
15-
description: "Manage the position to create and update.",
16-
},
17-
{
18-
path: "/home/settings/permissions",
19-
imageName: "icon-key",
20-
imageAlt: "Icon Permissions",
21-
title: "Permissions",
22-
description: "Setup a permission to roles.",
11+
path: "/home/settings/account-settings",
12+
imageName: "icon-cog",
13+
imageAlt: "Icon Account",
14+
title: "Account Settings",
15+
description: "Update your account information.",
2316
},
17+
];
18+
19+
export const adminControlsCardData: SettingsCardDataProps[] = [
2420
{
2521
path: "/home/settings/news-banners",
2622
imageName: "icon-bell",
@@ -42,11 +38,4 @@ export const settingsCardData: SettingsCardDataProps[] = [
4238
title: "Services",
4339
description: "View all services and download as PDF.",
4440
},
45-
{
46-
path: "/home/settings/account-settings",
47-
imageName: "icon-cog",
48-
imageAlt: "Icon Account",
49-
title: "Account Settings",
50-
description: "Update your account information.",
51-
}
5241
];

‎apps/codebility/constants/sidebar.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,12 @@ export const getSidebarData = async (
196196
label: "Hire",
197197
permission: "clients" as PermissionKey,
198198
},
199+
{
200+
route: pathsConfig.app.admin_controls,
201+
imgURL: "/assets/svgs/icon-admin-dashboard.svg",
202+
label: "Admin Controls",
203+
permission: "applicants" as PermissionKey,
204+
},
199205
{
200206
route: pathsConfig.app.settings,
201207
imgURL: "/assets/svgs/icon-cog.svg",

‎apps/codebility/middleware.ts‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,9 @@ const routePermissionMap: Record<string, keyof RolePermissions> = {
4646
"/home/clients": "clients",
4747
"/home/hire": "clients",
4848
"/home/projects": "projects",
49-
"/home/settings/permissions": "permissions",
50-
"/home/settings/roles": "roles",
5149
"/home/settings/profile": "resume",
5250
"/home/settings": "settings",
51+
"/home/admin-controls": "applicants",
5352
};
5453

5554
type RolePermissions = {
@@ -64,8 +63,6 @@ type RolePermissions = {
6463
settings: boolean;
6564
orgchart: boolean;
6665
resume?: boolean;
67-
permissions?: boolean;
68-
roles?: boolean;
6966
};
7067

7168
export async function middleware(req: NextRequest) {
@@ -218,8 +215,6 @@ export async function middleware(req: NextRequest) {
218215
"settings",
219216
"orgchart",
220217
"resume",
221-
"permissions",
222-
"roles",
223218
];
224219
return validPermissions.includes(permission as keyof RolePermissions);
225220
}
@@ -236,7 +231,7 @@ export async function middleware(req: NextRequest) {
236231
const { data: rolePermissions, error: roleError } = await supabase
237232
.from("roles")
238233
.select(
239-
"dashboard, kanban, time_tracker, interns, applicants, inhouse, clients, projects, settings, orgchart, roles, permissions, resume",
234+
"dashboard, kanban, time_tracker, interns, applicants, inhouse, clients, projects, settings, orgchart, resume",
240235
)
241236
.eq("id", role_id)
242237
.single();

‎apps/codebility/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@
4646
"@tanstack/react-query": "^5.28.9",
4747
"@tanstack/react-table": "^8.20.5",
4848
"@tiptap/extension-code-block-lowlight": "^2.27.1",
49+
"@tiptap/extension-color": "^2.27.1",
4950
"@tiptap/extension-highlight": "^2.11.9",
5051
"@tiptap/extension-image": "^2.11.9",
5152
"@tiptap/extension-placeholder": "^2.27.1",
5253
"@tiptap/extension-text-align": "^2.11.9",
54+
"@tiptap/extension-text-style": "^2.27.1",
5355
"@tiptap/pm": "^2.11.9",
5456
"@tiptap/react": "^2.26.4",
5557
"@tiptap/starter-kit": "^2.26.4",

‎apps/codebility/public/assets/svgs/icon-key.svg‎

Lines changed: 0 additions & 3 deletions
This file was deleted.

‎apps/codebility/public/assets/svgs/icon-roles.svg‎

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)