Spaces:
Running
Running
Download src/components/AdminUsersModal.tsx from harma-boutique-2026/Notes: direct link, hf CLI and curl.
- Browser
- Download file 19.1 kB
-
https://huggingface.co/spaces/harma-boutique-2026/Notes/resolve/main/src/components/AdminUsersModal.tsx
- Command line
-
hf download hf://spaces/harma-boutique-2026/Notes/src/components/AdminUsersModal.tsx
-
curl -L -o AdminUsersModal.tsx https://huggingface.co/spaces/harma-boutique-2026/Notes/resolve/main/src/components/AdminUsersModal.tsx
19.1 kB
| import React, { useState, useEffect } from 'react'; | |
| import { | |
| X, | |
| UserPlus, | |
| Shield, | |
| Key, | |
| Trash2, | |
| CheckCircle2, | |
| AlertCircle, | |
| Users, | |
| Lock, | |
| Edit2, | |
| Sliders, | |
| } from 'lucide-react'; | |
| import { User, UserPermissions } from '../types'; | |
| import { api } from '../services/api'; | |
| import { SCHOOL_SUBJECTS } from '../data/subjects'; | |
| interface AdminUsersModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| currentUser: User; | |
| } | |
| export const AdminUsersModal: React.FC<AdminUsersModalProps> = ({ isOpen, onClose, currentUser }) => { | |
| const [users, setUsers] = useState<User[]>([]); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [success, setSuccess] = useState<string | null>(null); | |
| // Form State for New Account Creation | |
| const [newUsername, setNewUsername] = useState(''); | |
| const [newPassword, setNewPassword] = useState(''); | |
| const [newRole, setNewRole] = useState<'admin' | 'user'>('user'); | |
| const [canEditDeleteOthers, setCanEditDeleteOthers] = useState(false); | |
| const [canManageUsersRestricted, setCanManageUsersRestricted] = useState(false); | |
| const [selectedGroups, setSelectedGroups] = useState<string[]>(['ALL']); | |
| // State for Editing Password / Special Perms | |
| const [editingUser, setEditingUser] = useState<User | null>(null); | |
| const [editPassword, setEditPassword] = useState(''); | |
| const isFullAdmin = currentUser.role === 'admin'; | |
| const loadUsers = async () => { | |
| setLoading(true); | |
| try { | |
| const u = await api.getUsers(); | |
| setUsers(u); | |
| } catch (err: any) { | |
| setError(err.message || 'Failed to load user accounts'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| if (isOpen) { | |
| loadUsers(); | |
| } | |
| }, [isOpen]); | |
| if (!isOpen) return null; | |
| const handleCreateUser = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!newUsername.trim() || !newPassword.trim()) { | |
| setError('Username and initial password are required'); | |
| return; | |
| } | |
| setError(null); | |
| setSuccess(null); | |
| const permissions: UserPermissions = { | |
| canEditDeleteOthers, | |
| canManageUsersRestricted, | |
| allowedGroups: selectedGroups.length > 0 ? selectedGroups : ['ALL'], | |
| }; | |
| try { | |
| const res = await api.createUser({ | |
| username: newUsername.trim(), | |
| password: newPassword.trim(), | |
| role: isFullAdmin ? newRole : 'user', | |
| permissions, | |
| }); | |
| setSuccess(res.message); | |
| setNewUsername(''); | |
| setNewPassword(''); | |
| setCanEditDeleteOthers(false); | |
| setCanManageUsersRestricted(false); | |
| await loadUsers(); | |
| } catch (err: any) { | |
| setError(err.message || 'Failed to create user account'); | |
| } | |
| }; | |
| const handleSaveUserUpdate = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!editingUser) return; | |
| setError(null); | |
| setSuccess(null); | |
| try { | |
| const message = await api.updateUser(editingUser.username, { | |
| password: editPassword.trim() || undefined, | |
| permissions: editingUser.permissions, | |
| role: editingUser.role, | |
| }); | |
| setSuccess(message); | |
| setEditingUser(null); | |
| setEditPassword(''); | |
| await loadUsers(); | |
| } catch (err: any) { | |
| setError(err.message || 'Failed to update user account'); | |
| } | |
| }; | |
| const handleDeleteUser = async (username: string) => { | |
| if (!window.confirm(`Are you sure you want to permanently delete account "${username}"?`)) return; | |
| setError(null); | |
| setSuccess(null); | |
| try { | |
| const message = await api.deleteUser(username); | |
| setSuccess(message); | |
| await loadUsers(); | |
| } catch (err: any) { | |
| setError(err.message || 'Failed to delete user account'); | |
| } | |
| }; | |
| const toggleGroupPermission = (grp: string) => { | |
| if (grp === 'ALL') { | |
| setSelectedGroups(['ALL']); | |
| return; | |
| } | |
| let current = selectedGroups.filter((g) => g !== 'ALL'); | |
| if (current.includes(grp)) { | |
| current = current.filter((g) => g !== grp); | |
| } else { | |
| current.push(grp); | |
| } | |
| if (current.length === 0) current = ['ALL']; | |
| setSelectedGroups(current); | |
| }; | |
| return ( | |
| <div className="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4"> | |
| <div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-3xl max-w-4xl w-full p-6 shadow-2xl relative overflow-hidden max-h-[90vh] flex flex-col"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800 shrink-0"> | |
| <div className="flex items-center gap-2.5"> | |
| <div className="p-2.5 rounded-xl bg-indigo-100 dark:bg-indigo-950 text-indigo-600 dark:text-indigo-400"> | |
| <Shield className="w-5 h-5" /> | |
| </div> | |
| <div> | |
| <h3 className="font-bold text-base text-slate-900 dark:text-white"> | |
| User Account Management & Special Permissions | |
| </h3> | |
| <p className="text-xs text-slate-500 dark:text-slate-400"> | |
| Admins create student accounts & configure special permissions saved in /Archives/users.json | |
| </p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="p-1.5 rounded-xl text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" | |
| > | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| {error && ( | |
| <div className="mt-4 p-3.5 rounded-2xl bg-rose-50 dark:bg-rose-950/80 border border-rose-200 dark:border-rose-800 text-rose-700 dark:text-rose-300 text-xs flex items-center gap-2 shrink-0"> | |
| <AlertCircle className="w-4 h-4 shrink-0" /> | |
| <span>{error}</span> | |
| </div> | |
| )} | |
| {success && ( | |
| <div className="mt-4 p-3.5 rounded-2xl bg-emerald-50 dark:bg-emerald-950/80 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300 text-xs flex items-center gap-2 shrink-0"> | |
| <CheckCircle2 className="w-4 h-4 shrink-0" /> | |
| <span>{success}</span> | |
| </div> | |
| )} | |
| {/* Modal Body Grid */} | |
| <div className="mt-4 grid grid-cols-1 md:grid-cols-12 gap-6 overflow-y-auto flex-1 pr-1"> | |
| {/* Left Column: Create New Account Form */} | |
| <div className="md:col-span-5 bg-slate-50 dark:bg-slate-800/50 p-4 rounded-2xl border border-slate-200 dark:border-slate-700/80 space-y-4"> | |
| <div className="flex items-center gap-2 pb-2 border-b border-slate-200 dark:border-slate-700"> | |
| <UserPlus className="w-4 h-4 text-indigo-600 dark:text-indigo-400" /> | |
| <h4 className="font-bold text-xs uppercase tracking-wider text-slate-800 dark:text-slate-200"> | |
| Create Account (Admin Rights) | |
| </h4> | |
| </div> | |
| <form onSubmit={handleCreateUser} className="space-y-3"> | |
| <div> | |
| <label className="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1"> | |
| Username * | |
| </label> | |
| <input | |
| type="text" | |
| required | |
| value={newUsername} | |
| onChange={(e) => setNewUsername(e.target.value)} | |
| placeholder="e.g. Rahul_S, Priya_Maths" | |
| className="w-full px-3 py-2 text-xs rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1"> | |
| Initial Password * | |
| </label> | |
| <input | |
| type="password" | |
| required | |
| value={newPassword} | |
| onChange={(e) => setNewPassword(e.target.value)} | |
| placeholder="Set account password" | |
| className="w-full px-3 py-2 text-xs rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" | |
| /> | |
| </div> | |
| {isFullAdmin && ( | |
| <div> | |
| <label className="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1"> | |
| System Role | |
| </label> | |
| <select | |
| value={newRole} | |
| onChange={(e) => setNewRole(e.target.value as 'admin' | 'user')} | |
| className="w-full px-3 py-2 text-xs font-medium rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" | |
| > | |
| <option value="user">Regular Student (User)</option> | |
| <option value="admin">Administrator (Harsha / Anirudh status)</option> | |
| </select> | |
| </div> | |
| )} | |
| {/* Special Permissions Config */} | |
| <div className="pt-2 border-t border-slate-200 dark:border-slate-700 space-y-2"> | |
| <span className="block text-[11px] font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider"> | |
| Special Permission Rights | |
| </span> | |
| <label className="flex items-start gap-2 cursor-pointer text-xs text-slate-700 dark:text-slate-300"> | |
| <input | |
| type="checkbox" | |
| checked={canEditDeleteOthers} | |
| onChange={(e) => setCanEditDeleteOthers(e.target.checked)} | |
| className="mt-0.5 rounded text-indigo-600 focus:ring-indigo-500" | |
| /> | |
| <span> | |
| <strong>Edit/Delete Shared Files:</strong> Right to remove or modify notes uploaded by other students. | |
| </span> | |
| </label> | |
| <label className="flex items-start gap-2 cursor-pointer text-xs text-slate-700 dark:text-slate-300"> | |
| <input | |
| type="checkbox" | |
| checked={canManageUsersRestricted} | |
| onChange={(e) => setCanManageUsersRestricted(e.target.checked)} | |
| className="mt-0.5 rounded text-indigo-600 focus:ring-indigo-500" | |
| /> | |
| <span> | |
| <strong>Restricted User Mgmt:</strong> Right to create student accounts or reset passwords in a restricted way. | |
| </span> | |
| </label> | |
| {canEditDeleteOthers && ( | |
| <div className="mt-2 pt-2"> | |
| <label className="block text-[11px] font-bold text-slate-600 dark:text-slate-400 mb-1"> | |
| Allowed Group Categories: | |
| </label> | |
| <div className="flex flex-wrap gap-1"> | |
| {['ALL', ...SCHOOL_SUBJECTS.map((s) => s.id)].map((subId) => ( | |
| <button | |
| key={subId} | |
| type="button" | |
| onClick={() => toggleGroupPermission(subId)} | |
| className={`text-[10px] font-bold px-2 py-0.5 rounded-lg border transition-all cursor-pointer ${ | |
| selectedGroups.includes(subId) | |
| ? 'bg-indigo-600 text-white border-indigo-600' | |
| : 'bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-400 border-slate-200 dark:border-slate-700' | |
| }`} | |
| > | |
| {subId} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <button | |
| type="submit" | |
| className="w-full py-2.5 px-4 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-xl text-xs shadow-md shadow-indigo-600/20 transition-all cursor-pointer" | |
| > | |
| Create Account & Save | |
| </button> | |
| </form> | |
| </div> | |
| {/* Right Column: Registered Accounts List */} | |
| <div className="md:col-span-7 space-y-3"> | |
| <div className="flex items-center justify-between pb-2 border-b border-slate-200 dark:border-slate-700"> | |
| <div className="flex items-center gap-2"> | |
| <Users className="w-4 h-4 text-indigo-600 dark:text-indigo-400" /> | |
| <h4 className="font-bold text-xs uppercase tracking-wider text-slate-800 dark:text-slate-200"> | |
| Registered Accounts ({users.length}) | |
| </h4> | |
| </div> | |
| </div> | |
| <div className="space-y-2 max-h-[450px] overflow-y-auto pr-1"> | |
| {users.map((u) => { | |
| const isCurrentAdminAccount = u.username.toLowerCase() === 'harsha' || u.username.toLowerCase() === 'anirudh'; | |
| return ( | |
| <div | |
| key={u.username} | |
| className="p-3 bg-slate-50 dark:bg-slate-800/80 rounded-2xl border border-slate-200 dark:border-slate-700/80 flex items-center justify-between gap-2" | |
| > | |
| <div className="min-w-0 flex-1"> | |
| <div className="flex items-center gap-2"> | |
| <span className="font-bold text-xs text-slate-900 dark:text-white truncate"> | |
| {u.username} | |
| </span> | |
| {u.role === 'admin' ? ( | |
| <span className="px-1.5 py-0.5 text-[9px] font-bold uppercase rounded bg-amber-100 text-amber-800 dark:bg-amber-900/60 dark:text-amber-300"> | |
| Admin | |
| </span> | |
| ) : ( | |
| <span className="px-1.5 py-0.5 text-[9px] font-bold uppercase rounded bg-slate-200 text-slate-700 dark:bg-slate-700 dark:text-slate-300"> | |
| User | |
| </span> | |
| )} | |
| </div> | |
| {/* Display Passwords to Full Admin */} | |
| {isFullAdmin && u.password && ( | |
| <p className="text-[11px] text-indigo-600 dark:text-indigo-400 font-mono mt-0.5"> | |
| Pass: {u.password} | |
| </p> | |
| )} | |
| {/* Display Special Permissions */} | |
| <div className="flex flex-wrap gap-1 mt-1"> | |
| {u.permissions?.canEditDeleteOthers && ( | |
| <span className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300"> | |
| Edit/Delete Files ({u.permissions.allowedGroups?.join(', ') || 'ALL'}) | |
| </span> | |
| )} | |
| {u.permissions?.canManageUsersRestricted && ( | |
| <span className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"> | |
| Restricted Mgmt | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| {/* Actions */} | |
| <div className="flex items-center gap-1.5"> | |
| <button | |
| onClick={() => { | |
| setEditingUser(u); | |
| setEditPassword(u.password || ''); | |
| }} | |
| className="p-1.5 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" | |
| title="Update Password / Permissions" | |
| > | |
| <Edit2 className="w-3.5 h-3.5" /> | |
| </button> | |
| {!isCurrentAdminAccount && isFullAdmin && ( | |
| <button | |
| onClick={() => handleDeleteUser(u.username)} | |
| className="p-1.5 rounded-lg text-rose-600 dark:text-rose-400 hover:bg-rose-100 dark:hover:bg-rose-950/60 transition-colors" | |
| title="Delete User Account" | |
| > | |
| <Trash2 className="w-3.5 h-3.5" /> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Edit Password Sub-modal */} | |
| {editingUser && ( | |
| <div className="absolute inset-0 z-50 bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-4"> | |
| <div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-3xl p-6 max-w-md w-full space-y-4"> | |
| <h4 className="font-bold text-sm text-slate-900 dark:text-white"> | |
| Update Account for {editingUser.username} | |
| </h4> | |
| <form onSubmit={handleSaveUserUpdate} className="space-y-3"> | |
| <div> | |
| <label className="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1"> | |
| New Password | |
| </label> | |
| <input | |
| type="password" | |
| required | |
| value={editPassword} | |
| onChange={(e) => setEditPassword(e.target.value)} | |
| placeholder="Enter new password" | |
| className="w-full px-3 py-2 text-xs rounded-xl border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" | |
| /> | |
| </div> | |
| <div className="flex justify-end gap-2 pt-2"> | |
| <button | |
| type="button" | |
| onClick={() => setEditingUser(null)} | |
| className="px-3 py-1.5 text-xs font-semibold text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| type="submit" | |
| className="px-4 py-1.5 text-xs font-semibold text-white bg-indigo-600 hover:bg-indigo-500 rounded-xl shadow" | |
| > | |
| Save Changes | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |