Add local user authentication and admin capabilities
Implement local user authentication with password hashing, add admin roles for user management and audit log viewing, and introduce audit logging for critical actions. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 776963d0-f75d-42e2-a57b-cc36bdff8495 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 832a44ff-12ae-4096-8a0d-666ec083d536 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0683fb79-a27c-485c-9333-5f4b288c4567/776963d0-f75d-42e2-a57b-cc36bdff8495/1p7jhzu Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -27,8 +27,20 @@ import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
|
||||
import { ExternalLink, Star, ArrowLeft, Plus } from "lucide-react";
|
||||
import { ExternalLink, Star, ArrowLeft, Plus, Pencil, Trash2 } from "lucide-react";
|
||||
import { Link } from "wouter";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useDeleteTool, getListToolsQueryKey } from "@workspace/api-client-react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
const ratingSchema = z.object({
|
||||
usefulness: z.number().min(1).max(5),
|
||||
@@ -47,6 +59,10 @@ export default function ToolDetail() {
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
const [isReviewFormOpen, setIsReviewFormOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
|
||||
const { user, isAdmin } = useAuth();
|
||||
const deleteTool = useDeleteTool();
|
||||
|
||||
const { data: tool, isLoading: loadingTool } = useGetTool(id, {
|
||||
query: { enabled: !!id, queryKey: getGetToolQueryKey(id) }
|
||||
@@ -99,7 +115,7 @@ export default function ToolDetail() {
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Failed to submit rating",
|
||||
description: error.error || "An unexpected error occurred.",
|
||||
description: error.data?.error || error.message || "An unexpected error occurred.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
@@ -122,7 +138,32 @@ export default function ToolDetail() {
|
||||
const usefulnessData = distribution?.usefulness.map(b => ({ score: b.score, count: b.count })).reverse() || [];
|
||||
const usabilityData = distribution?.usability.map(b => ({ score: b.score, count: b.count })).reverse() || [];
|
||||
|
||||
function canEdit(toolData: { createdBy?: string | null }): boolean {
|
||||
if (!user) return false;
|
||||
if (isAdmin) return true;
|
||||
return toolData.createdBy === user.sub ||
|
||||
toolData.createdBy === user.preferredUsername;
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
deleteTool.mutate(
|
||||
{ id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({ title: "Tool deleted" });
|
||||
queryClient.invalidateQueries({ queryKey: getListToolsQueryKey() });
|
||||
setLocation("/tools");
|
||||
},
|
||||
onError: (err) => {
|
||||
toast({ title: "Failed to delete", description: err.data?.error || err.message, variant: "destructive" });
|
||||
setDeleteOpen(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<div className="space-y-6 max-w-5xl mx-auto pb-10">
|
||||
<Button variant="ghost" asChild className="mb-2 -ml-4 text-muted-foreground">
|
||||
@@ -183,7 +224,7 @@ export default function ToolDetail() {
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Based on {tool.ratingCount} reviews
|
||||
</div>
|
||||
|
||||
|
||||
{tool.websiteUrl && (
|
||||
<Button asChild className="w-full mt-2" variant="outline">
|
||||
<a href={tool.websiteUrl} target="_blank" rel="noopener noreferrer">
|
||||
@@ -191,6 +232,24 @@ export default function ToolDetail() {
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canEdit(tool) && (
|
||||
<div className="flex gap-2 w-full mt-1">
|
||||
<Button asChild variant="outline" size="sm" className="flex-1 gap-2">
|
||||
<Link href={`/tools/${id}/edit`}>
|
||||
<Pencil className="w-3.5 h-3.5" /> Edit
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 gap-2 text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" /> Delete
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -423,5 +482,27 @@ export default function ToolDetail() {
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete this tool?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently remove <span className="font-medium">{tool?.name}</span> and all its ratings. This cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={handleDelete}
|
||||
disabled={deleteTool.isPending}
|
||||
>
|
||||
{deleteTool.isPending ? "Deleting…" : "Delete"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user