Skip to content

Commit 7813520

Browse files
authored
Merge pull request #95 from replicatedhq/devin/1746637242-admin-user-privileges
feature: Add toggle for user admin privileges
2 parents 948374f + 682fa1a commit 7813520

File tree

3 files changed

+137
-11
lines changed

3 files changed

+137
-11
lines changed

chartsmith-app/app/admin/users/page.tsx

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import React, { useState, useEffect } from "react";
44
import { useSession } from "@/app/hooks/useSession";
55
import { listUserAdminAction } from "@/lib/auth/actions/list-users";
6+
import { updateUserAdminStatusAction } from "@/lib/auth/actions/update-admin-status";
67
import { UserAdmin } from "@/lib/types/user";
7-
import { ArrowUpDown, Loader2, Shield, FolderKanban } from "lucide-react";
8+
import { ArrowUpDown, Loader2, Shield, FolderKanban, X, Check } from "lucide-react";
89
import Image from "next/image";
910
import Link from "next/link";
1011

@@ -21,6 +22,7 @@ export default function UsersPage() {
2122
const [sortField, setSortField] = useState<SortField>("lastActive");
2223
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
2324
const [loading, setLoading] = useState(true);
25+
const [confirmDialog, setConfirmDialog] = useState<{ open: boolean; userId: string; newStatus: boolean } | null>(null);
2426

2527
// Load users and workspace counts
2628
useEffect(() => {
@@ -51,6 +53,33 @@ export default function UsersPage() {
5153
setSortDirection("asc");
5254
}
5355
};
56+
57+
// Handle admin status toggle
58+
const handleAdminToggle = async (userId: string, newStatus: boolean) => {
59+
setConfirmDialog({ open: true, userId, newStatus });
60+
};
61+
62+
// Handle confirmation of admin status change
63+
const handleConfirmAdminChange = async () => {
64+
if (!confirmDialog || !session) return;
65+
66+
try {
67+
const success = await updateUserAdminStatusAction(session, confirmDialog.userId, confirmDialog.newStatus);
68+
if (success) {
69+
setUsers(prevUsers =>
70+
prevUsers.map(user =>
71+
user.id === confirmDialog.userId
72+
? { ...user, isAdmin: confirmDialog.newStatus }
73+
: user
74+
)
75+
);
76+
}
77+
} catch (error) {
78+
console.error("Failed to update admin status:", error);
79+
} finally {
80+
setConfirmDialog(null);
81+
}
82+
};
5483

5584
// Sort users
5685
const sortedUsers = [...users].sort((a, b) => {
@@ -199,14 +228,22 @@ export default function UsersPage() {
199228
{formatDate(user.lastActiveAt)}
200229
</td>
201230
<td className="px-4 py-3 text-sm text-text">
202-
{user.isAdmin ? (
203-
<div className="flex items-center text-primary">
204-
<Shield className="w-4 h-4 mr-1" />
205-
Admin
206-
</div>
207-
) : (
208-
"User"
209-
)}
231+
<div className="flex items-center">
232+
<input
233+
type="checkbox"
234+
checked={user.isAdmin || false}
235+
onChange={() => handleAdminToggle(user.id, !user.isAdmin)}
236+
className="mr-2 h-4 w-4 rounded border-border text-primary focus:ring-primary"
237+
/>
238+
{user.isAdmin ? (
239+
<div className="flex items-center text-primary">
240+
<Shield className="w-4 h-4 mr-1" />
241+
Admin
242+
</div>
243+
) : (
244+
"User"
245+
)}
246+
</div>
210247
</td>
211248
</tr>
212249
))}
@@ -215,6 +252,40 @@ export default function UsersPage() {
215252
</div>
216253
)}
217254
</div>
255+
256+
{/* Confirmation Dialog */}
257+
{confirmDialog && confirmDialog.open && (
258+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
259+
<div className="bg-surface p-6 rounded-lg border border-border max-w-md w-full">
260+
<div className="flex justify-between items-center mb-4">
261+
<h3 className="text-lg font-medium">Confirm Admin Status Change</h3>
262+
<button
263+
onClick={() => setConfirmDialog(null)}
264+
className="text-text/50 hover:text-text"
265+
>
266+
<X className="w-5 h-5" />
267+
</button>
268+
</div>
269+
<p className="mb-6">
270+
Are you sure you want to {confirmDialog.newStatus ? "grant" : "revoke"} admin privileges for this user?
271+
</p>
272+
<div className="flex justify-end space-x-4">
273+
<button
274+
onClick={() => setConfirmDialog(null)}
275+
className="px-4 py-2 border border-border rounded-md"
276+
>
277+
Cancel
278+
</button>
279+
<button
280+
onClick={handleConfirmAdminChange}
281+
className="px-4 py-2 bg-primary text-white rounded-md flex items-center"
282+
>
283+
<Check className="w-4 h-4 mr-2" /> Confirm
284+
</button>
285+
</div>
286+
</div>
287+
</div>
288+
)}
218289
</div>
219290
);
220-
}
291+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use server";
2+
3+
import { updateUserAdminStatus } from "../user";
4+
import { Session } from "../../types/session";
5+
import { logger } from "../../utils/logger";
6+
7+
export async function updateUserAdminStatusAction(session: Session, userId: string, isAdmin: boolean): Promise<boolean> {
8+
try {
9+
// Only admins can change admin status
10+
if (!session.user.isAdmin) {
11+
logger.warn("Non-admin user attempted to update admin status", {
12+
requestingUserId: session.user.id,
13+
targetUserId: userId,
14+
});
15+
return false;
16+
}
17+
18+
// Prevent admins from modifying their own admin status
19+
if (session.user.id === userId) {
20+
logger.warn("Admin cannot modify their own admin status", {
21+
userId: session.user.id,
22+
});
23+
return false;
24+
}
25+
26+
const success = await updateUserAdminStatus(userId, isAdmin);
27+
return success;
28+
} catch (error) {
29+
logger.error("Failed to update user admin status", { error, userId });
30+
return false;
31+
}
32+
}

chartsmith-app/lib/auth/user.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,4 +518,27 @@ export async function approveWaitlistUser(waitlistId: string): Promise<boolean>
518518
logger.error("Failed to approve waitlist user", { waitlistId, error });
519519
return false;
520520
}
521-
}
521+
}
522+
523+
/**
524+
* Updates a user's admin status
525+
* @param userId The ID of the user to update
526+
* @param isAdmin Whether the user should have admin privileges
527+
* @returns True if the update was successful, false otherwise
528+
*/
529+
export async function updateUserAdminStatus(userId: string, isAdmin: boolean): Promise<boolean> {
530+
try {
531+
const db = getDB(await getParam("DB_URI"));
532+
await db.query(
533+
`UPDATE chartsmith_user
534+
SET is_admin = $1
535+
WHERE id = $2`,
536+
[isAdmin, userId]
537+
);
538+
logger.info("Updated user admin status", { userId, isAdmin });
539+
return true;
540+
} catch (err) {
541+
logger.error("Failed to update user admin status", { err, userId });
542+
return false;
543+
}
544+
}

0 commit comments

Comments
 (0)