Notes / src /components /AdminUsersModal.tsx
harma-boutique-2026's picture
Upload 25 files
01f262b verified
Raw History Blame Contribute Delete
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 &amp; Special Permissions
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400">
Admins create student accounts &amp; 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 &amp; 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>
);
};