mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 20:52:09 +01:00
Major code quality overhaul addressing 58% of all linting issues: • Type Safety Improvements: - Replace all any types with proper TypeScript interfaces - Fix Map component shadowing (renamed to CountryMap) - Add comprehensive custom error classes system - Enhance API route type safety • Accessibility Enhancements: - Add explicit button types to all interactive elements - Implement useId() hooks for form element accessibility - Add SVG title attributes for screen readers - Fix static element interactions with keyboard handlers • React Best Practices: - Resolve exhaustive dependencies warnings with useCallback - Extract nested component definitions to top level - Fix array index keys with proper unique identifiers - Improve component organization and prop typing • Code Organization: - Automatic import organization and type import optimization - Fix unused function parameters and variables - Enhanced error handling with structured error responses - Improve component reusability and maintainability Results: 248 → 104 total issues (58% reduction) - Fixed all critical type safety and security issues - Enhanced accessibility compliance significantly - Improved code maintainability and performance
274 lines
8.2 KiB
TypeScript
274 lines
8.2 KiB
TypeScript
"use client";
|
|
|
|
import { AlertCircle, Eye, Shield, UserPlus, Users } from "lucide-react";
|
|
import { useSession } from "next-auth/react";
|
|
import { useCallback, useEffect, useId, useState } from "react";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
|
|
interface UserItem {
|
|
id: string;
|
|
email: string;
|
|
role: string;
|
|
}
|
|
|
|
export default function UserManagementPage() {
|
|
const { data: session, status } = useSession();
|
|
const [users, setUsers] = useState<UserItem[]>([]);
|
|
const [email, setEmail] = useState<string>("");
|
|
const [role, setRole] = useState<string>("USER");
|
|
const [message, setMessage] = useState<string>("");
|
|
const [loading, setLoading] = useState(true);
|
|
const emailId = useId();
|
|
|
|
const fetchUsers = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/dashboard/users");
|
|
const data = await res.json();
|
|
setUsers(data.users);
|
|
} catch (error) {
|
|
console.error("Failed to fetch users:", error);
|
|
setMessage("Failed to load users.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (status === "authenticated") {
|
|
if (session?.user?.role === "ADMIN") {
|
|
fetchUsers();
|
|
} else {
|
|
setLoading(false); // Stop loading for non-admin users
|
|
}
|
|
} else if (status === "unauthenticated") {
|
|
setLoading(false);
|
|
}
|
|
}, [status, session?.user?.role, fetchUsers]);
|
|
|
|
async function inviteUser() {
|
|
setMessage("");
|
|
try {
|
|
const res = await fetch("/api/dashboard/users", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, role }),
|
|
});
|
|
|
|
if (res.ok) {
|
|
setMessage("User invited successfully!");
|
|
setEmail(""); // Clear the form
|
|
// Refresh the user list
|
|
fetchUsers();
|
|
} else {
|
|
const error = await res.json();
|
|
setMessage(
|
|
`Failed to invite user: ${error.message || "Unknown error"}`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
setMessage("Failed to invite user. Please try again.");
|
|
console.error("Error inviting user:", error);
|
|
}
|
|
}
|
|
|
|
// Loading state
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
Loading users...
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Check for admin access
|
|
if (session?.user?.role !== "ADMIN") {
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="text-center py-8">
|
|
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
|
|
<h2 className="font-bold text-xl text-destructive mb-2">
|
|
Access Denied
|
|
</h2>
|
|
<p className="text-muted-foreground">
|
|
You don't have permission to view user management.
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6" data-testid="user-management-page">
|
|
{/* Header */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Users className="h-6 w-6" />
|
|
User Management
|
|
</CardTitle>
|
|
</CardHeader>
|
|
</Card>
|
|
|
|
{/* Message Alert */}
|
|
{message && (
|
|
<Alert variant={message.includes("Failed") ? "destructive" : "default"}>
|
|
<AlertDescription>{message}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Invite New User */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<UserPlus className="h-5 w-5" />
|
|
Invite New User
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form
|
|
className="grid grid-cols-1 sm:grid-cols-3 gap-4 items-end"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
inviteUser();
|
|
}}
|
|
autoComplete="off"
|
|
data-testid="invite-form"
|
|
>
|
|
<div className="space-y-2">
|
|
<Label htmlFor={emailId}>Email</Label>
|
|
<Input
|
|
id={emailId}
|
|
type="email"
|
|
placeholder="user@example.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="role">Role</Label>
|
|
<Select value={role} onValueChange={setRole}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select role" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="USER">User</SelectItem>
|
|
<SelectItem value="ADMIN">Admin</SelectItem>
|
|
<SelectItem value="AUDITOR">Auditor</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<Button type="submit" className="gap-2">
|
|
<UserPlus className="h-4 w-4" />
|
|
Invite User
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Current Users */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Users className="h-5 w-5" />
|
|
Current Users ({users?.length || 0})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Email</TableHead>
|
|
<TableHead>Role</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{users.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={3}
|
|
className="text-center text-muted-foreground"
|
|
>
|
|
No users found
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
users.map((user) => (
|
|
<TableRow key={user.id}>
|
|
<TableCell className="font-medium">
|
|
{user.email}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
variant={
|
|
user.role === "ADMIN"
|
|
? "default"
|
|
: user.role === "AUDITOR"
|
|
? "secondary"
|
|
: "outline"
|
|
}
|
|
className="gap-1"
|
|
data-testid="role-badge"
|
|
>
|
|
{user.role === "ADMIN" && (
|
|
<Shield className="h-3 w-3" />
|
|
)}
|
|
{user.role === "AUDITOR" && (
|
|
<Eye className="h-3 w-3" />
|
|
)}
|
|
{user.role}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className="text-muted-foreground text-sm">
|
|
No actions available
|
|
</span>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|