Enhance DJ management with password reset and deactivation features
This commit introduces the ability to reset user passwords, both temporary and permanent, within the DJ management interface. It also adds soft deactivation and hard deletion options for users. The InvitationManagement component has been updated to improve clarity and user experience during the user creation process. Backend changes include password hashing using bcrypt, new API endpoints for status checks and password management, and modifications to storage functions to handle sanitized user data and password verification. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 3a22ac80-cd1d-4441-9e36-f24fc2f4c3de Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3478f7c3-db8c-4fca-9165-3adbdf1b5829/3a22ac80-cd1d-4441-9e36-f24fc2f4c3de/Yn4xAWn
This commit is contained in:
@@ -14,6 +14,10 @@ run = ["npm", "run", "start"]
|
|||||||
localPort = 5000
|
localPort = 5000
|
||||||
externalPort = 80
|
externalPort = 80
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 34677
|
||||||
|
externalPort = 3000
|
||||||
|
|
||||||
[workflows]
|
[workflows]
|
||||||
runButton = "Project"
|
runButton = "Project"
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,19 @@ import { DataTable } from "@/components/ui/data-table";
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
import { UserPlus, UserCheck, UserX, Shield, ShieldOff } from "lucide-react";
|
import { UserPlus, UserCheck, UserX, Shield, ShieldOff, Key, UserMinus, Trash2 } from "lucide-react";
|
||||||
import { InvitationManagement } from "./InvitationManagement";
|
import { InvitationManagement } from "./InvitationManagement";
|
||||||
|
|
||||||
export function DJManagement() {
|
export function DJManagement() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||||
|
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||||
const [inviteName, setInviteName] = useState("");
|
const [inviteName, setInviteName] = useState("");
|
||||||
const [inviteEmail, setInviteEmail] = useState("");
|
const [inviteEmail, setInviteEmail] = useState("");
|
||||||
|
const [selectedUser, setSelectedUser] = useState<any>(null);
|
||||||
|
const [newPassword, setNewPassword] = useState("");
|
||||||
|
const [isTemporaryPassword, setIsTemporaryPassword] = useState(true);
|
||||||
|
|
||||||
const { data: djs, isLoading } = useQuery({
|
const { data: djs, isLoading } = useQuery({
|
||||||
queryKey: ["/api/users"],
|
queryKey: ["/api/users"],
|
||||||
@@ -91,6 +95,72 @@ export function DJManagement() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setPasswordMutation = useMutation({
|
||||||
|
mutationFn: async (data: { userId: string; password: string; isTemporary: boolean }) => {
|
||||||
|
await apiRequest("POST", `/api/users/${data.userId}/set-password`, {
|
||||||
|
password: data.password,
|
||||||
|
isTemporary: data.isTemporary
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Password updated",
|
||||||
|
description: "User password has been updated successfully.",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/users"] });
|
||||||
|
setShowPasswordModal(false);
|
||||||
|
setNewPassword("");
|
||||||
|
setSelectedUser(null);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const softDeactivateMutation = useMutation({
|
||||||
|
mutationFn: async (userId: string) => {
|
||||||
|
await apiRequest("POST", `/api/users/${userId}/deactivate-soft`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "User deactivated",
|
||||||
|
description: "User has been deactivated (data preserved).",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/users"] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hardDeleteMutation = useMutation({
|
||||||
|
mutationFn: async (userId: string) => {
|
||||||
|
await apiRequest("DELETE", `/api/users/${userId}/delete-hard`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "User deleted",
|
||||||
|
description: "User and future data have been permanently deleted.",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/users"] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleInvite = () => {
|
const handleInvite = () => {
|
||||||
if (!inviteName || !inviteEmail) {
|
if (!inviteName || !inviteEmail) {
|
||||||
toast({
|
toast({
|
||||||
@@ -134,11 +204,29 @@ export function DJManagement() {
|
|||||||
{
|
{
|
||||||
key: "isActive" as const,
|
key: "isActive" as const,
|
||||||
header: "Status",
|
header: "Status",
|
||||||
cell: (dj: any) => (
|
cell: (dj: any) => {
|
||||||
<Badge variant={dj.isActive ? "default" : "destructive"}>
|
// Use derivedActive which combines isActive flag + password presence (calculated server-side)
|
||||||
{dj.isActive ? "Active" : "Inactive"}
|
const derivedActive = dj.derivedActive;
|
||||||
</Badge>
|
const needsPasswordSetup = dj.isActive && !derivedActive;
|
||||||
),
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Badge variant={derivedActive ? "default" : "destructive"} data-testid={`status-${dj.id}`}>
|
||||||
|
{derivedActive ? "Active" : "Inactive"}
|
||||||
|
</Badge>
|
||||||
|
{dj.needsPasswordChange && (
|
||||||
|
<Badge variant="outline" className="text-xs" data-testid={`needs-password-change-${dj.id}`}>
|
||||||
|
Needs Password Change
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{needsPasswordSetup && (
|
||||||
|
<Badge variant="secondary" className="text-xs" data-testid={`needs-password-setup-${dj.id}`}>
|
||||||
|
Needs Password Setup
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "maxEventsPerMonth" as const,
|
key: "maxEventsPerMonth" as const,
|
||||||
@@ -146,31 +234,106 @@ export function DJManagement() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const actions = (dj: any) => (
|
const handleSetPassword = (user: any) => {
|
||||||
<div className="flex items-center space-x-2">
|
setSelectedUser(user);
|
||||||
<Button
|
setShowPasswordModal(true);
|
||||||
variant="outline"
|
};
|
||||||
size="sm"
|
|
||||||
onClick={() => toggleUserStatusMutation.mutate({
|
const handleSoftDeactivate = (user: any) => {
|
||||||
userId: dj.id,
|
if (window.confirm(`Are you sure you want to deactivate ${user.displayName || user.email}? This will hide them from scheduling but preserve their data.`)) {
|
||||||
action: dj.isActive ? "deactivate" : "activate"
|
softDeactivateMutation.mutate(user.id);
|
||||||
})}
|
}
|
||||||
>
|
};
|
||||||
{dj.isActive ? <UserX className="w-4 h-4" /> : <UserCheck className="w-4 h-4" />}
|
|
||||||
</Button>
|
const handleHardDelete = (user: any) => {
|
||||||
|
if (window.confirm(`DANGER: Are you sure you want to permanently delete ${user.displayName || user.email} and all their future data? This cannot be undone.`)) {
|
||||||
<Button
|
hardDeleteMutation.mutate(user.id);
|
||||||
variant="outline"
|
}
|
||||||
size="sm"
|
};
|
||||||
onClick={() => toggleAdminMutation.mutate({
|
|
||||||
userId: dj.id,
|
const generatePassword = () => {
|
||||||
action: dj.role === "admin" ? "remove-admin" : "make-admin"
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
|
||||||
})}
|
let password = "";
|
||||||
>
|
for (let i = 0; i < 12; i++) {
|
||||||
{dj.role === "admin" ? <ShieldOff className="w-4 h-4" /> : <Shield className="w-4 h-4" />}
|
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
</Button>
|
}
|
||||||
</div>
|
setNewPassword(password);
|
||||||
);
|
};
|
||||||
|
|
||||||
|
const actions = (dj: any) => {
|
||||||
|
// Use derivedActive which combines isActive flag + password presence
|
||||||
|
const derivedActive = dj.derivedActive;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{/* Set/Change Password */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleSetPassword(dj)}
|
||||||
|
title="Set/Change Password"
|
||||||
|
data-testid={`button-set-password-${dj.id}`}
|
||||||
|
>
|
||||||
|
<Key className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Reactivate (only show if user is inactive) */}
|
||||||
|
{!derivedActive && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleUserStatusMutation.mutate({
|
||||||
|
userId: dj.id,
|
||||||
|
action: "activate"
|
||||||
|
})}
|
||||||
|
title="Reactivate User"
|
||||||
|
data-testid={`button-reactivate-${dj.id}`}
|
||||||
|
>
|
||||||
|
<UserCheck className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Admin Toggle */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleAdminMutation.mutate({
|
||||||
|
userId: dj.id,
|
||||||
|
action: dj.role === "admin" ? "remove-admin" : "make-admin"
|
||||||
|
})}
|
||||||
|
title={dj.role === "admin" ? "Remove Admin" : "Make Admin"}
|
||||||
|
data-testid={`button-admin-toggle-${dj.id}`}
|
||||||
|
>
|
||||||
|
{dj.role === "admin" ? <ShieldOff className="w-4 h-4" /> : <Shield className="w-4 h-4" />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Soft Deactivate (only show if active) */}
|
||||||
|
{derivedActive && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleSoftDeactivate(dj)}
|
||||||
|
title="Deactivate (Keep Data)"
|
||||||
|
data-testid={`button-soft-deactivate-${dj.id}`}
|
||||||
|
>
|
||||||
|
<UserMinus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Hard Delete */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleHardDelete(dj)}
|
||||||
|
className="text-red-600 hover:text-red-700"
|
||||||
|
title="Permanently Delete"
|
||||||
|
data-testid={`button-hard-delete-${dj.id}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -224,13 +387,82 @@ export function DJManagement() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Password Management Dialog */}
|
||||||
|
<Dialog open={showPasswordModal} onOpenChange={setShowPasswordModal}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Set User Password</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 mb-2">
|
||||||
|
Setting a password for: <strong>{selectedUser?.displayName || selectedUser?.email}</strong>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Users are considered active when they have a password set.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="newPassword">New Password</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="newPassword"
|
||||||
|
type="text"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
placeholder="Enter new password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={generatePassword}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="isTemporary"
|
||||||
|
checked={isTemporaryPassword}
|
||||||
|
onChange={(e) => setIsTemporaryPassword(e.target.checked)}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="isTemporary" className="text-sm">
|
||||||
|
Require password change on next login
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<Button variant="outline" onClick={() => setShowPasswordModal(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => selectedUser && setPasswordMutation.mutate({
|
||||||
|
userId: selectedUser.id,
|
||||||
|
password: newPassword,
|
||||||
|
isTemporary: isTemporaryPassword
|
||||||
|
})}
|
||||||
|
disabled={setPasswordMutation.isPending || !newPassword}
|
||||||
|
>
|
||||||
|
{setPasswordMutation.isPending ? "Setting..." : "Set Password"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Active DJs</CardTitle>
|
<CardTitle>Active DJs</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={djs || []}
|
data={Array.isArray(djs) ? djs : []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
searchable
|
searchable
|
||||||
@@ -244,7 +476,7 @@ export function DJManagement() {
|
|||||||
<CardTitle>Invitation Management</CardTitle>
|
<CardTitle>Invitation Management</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<InvitationManagement invitations={invitations || []} />
|
<InvitationManagement invitations={Array.isArray(invitations) ? invitations : []} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function InvitationManagement({ invitations }: InvitationManagementProps)
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
description: "User created successfully. They can now log in with their temporary password.",
|
description: "User created and activated successfully. They can now log in with their initial password.",
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/users'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/users'] });
|
||||||
setIsManualCreateOpen(false);
|
setIsManualCreateOpen(false);
|
||||||
@@ -143,7 +143,7 @@ export function InvitationManagement({ invitations }: InvitationManagementProps)
|
|||||||
<h3 className="text-lg font-semibold">Invitation Management</h3>
|
<h3 className="text-lg font-semibold">Invitation Management</h3>
|
||||||
<Button onClick={handleManualCreate} className="bg-green-600 hover:bg-green-700">
|
<Button onClick={handleManualCreate} className="bg-green-600 hover:bg-green-700">
|
||||||
<UserPlus className="w-4 h-4 mr-2" />
|
<UserPlus className="w-4 h-4 mr-2" />
|
||||||
Manual Activate
|
Create User
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ export function InvitationManagement({ invitations }: InvitationManagementProps)
|
|||||||
<Dialog open={isManualCreateOpen} onOpenChange={setIsManualCreateOpen}>
|
<Dialog open={isManualCreateOpen} onOpenChange={setIsManualCreateOpen}>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Manual User Activation</DialogTitle>
|
<DialogTitle>Create New User</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -283,14 +283,14 @@ export function InvitationManagement({ invitations }: InvitationManagementProps)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="tempPassword">Temporary Password *</Label>
|
<Label htmlFor="tempPassword">Initial Password *</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
id="tempPassword"
|
id="tempPassword"
|
||||||
type="text"
|
type="text"
|
||||||
value={manualUserData.tempPassword}
|
value={manualUserData.tempPassword}
|
||||||
onChange={(e) => setManualUserData(prev => ({ ...prev, tempPassword: e.target.value }))}
|
onChange={(e) => setManualUserData(prev => ({ ...prev, tempPassword: e.target.value }))}
|
||||||
placeholder="Enter temporary password"
|
placeholder="Enter initial password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -303,7 +303,7 @@ export function InvitationManagement({ invitations }: InvitationManagementProps)
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
The user will need to change this password on first login.
|
User will be active once they have a password set. They will need to change this password on first login.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,13 +58,17 @@ export function ProfileModal({ isOpen, onClose }: ProfileModalProps) {
|
|||||||
|
|
||||||
const updateProfileMutation = useMutation({
|
const updateProfileMutation = useMutation({
|
||||||
mutationFn: async (data: any) => {
|
mutationFn: async (data: any) => {
|
||||||
|
// Update display name
|
||||||
const updates: any = { displayName: data.displayName };
|
const updates: any = { displayName: data.displayName };
|
||||||
|
|
||||||
if (data.newPassword) {
|
|
||||||
updates.password = data.newPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
await apiRequest("PATCH", `/api/users/${user?.id}`, updates);
|
await apiRequest("PATCH", `/api/users/${user?.id}`, updates);
|
||||||
|
|
||||||
|
// Handle password change separately using secure endpoint
|
||||||
|
if (data.newPassword) {
|
||||||
|
await apiRequest("POST", `/api/users/${user?.id}/set-password`, {
|
||||||
|
password: data.newPassword,
|
||||||
|
isTemporary: false
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
Generated
+38
-5
@@ -40,7 +40,9 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.0",
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/memoizee": "^0.4.12",
|
"@types/memoizee": "^0.4.12",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -3322,6 +3324,15 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcrypt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.5",
|
"version": "1.19.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||||
@@ -3753,6 +3764,20 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bcrypt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^8.3.0",
|
||||||
|
"node-gyp-build": "^4.8.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -6418,12 +6443,20 @@
|
|||||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/node-gyp-build": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "4.8.3",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||||
"integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==",
|
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || ^20 || >= 21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"node-gyp-build": "bin.js",
|
"node-gyp-build": "bin.js",
|
||||||
"node-gyp-build-optional": "optional.js",
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
|||||||
@@ -42,7 +42,9 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@radix-ui/react-tooltip": "^1.2.0",
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/memoizee": "^0.4.12",
|
"@types/memoizee": "^0.4.12",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
|||||||
+32
-2
@@ -613,7 +613,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: "User created successfully",
|
message: "User created successfully",
|
||||||
user: { ...user, tempPassword: undefined } // Don't send password back
|
user
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating user manually:", error);
|
console.error("Error creating user manually:", error);
|
||||||
@@ -628,6 +628,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
if (!password) {
|
if (!password) {
|
||||||
return res.status(400).json({ message: "Password is required" });
|
return res.status(400).json({ message: "Password is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Basic password strength validation
|
||||||
|
if (password.length < 8) {
|
||||||
|
return res.status(400).json({ message: "Password must be at least 8 characters long" });
|
||||||
|
}
|
||||||
|
|
||||||
await storage.setUserPassword(req.params.id, password, isTemporary || false);
|
await storage.setUserPassword(req.params.id, password, isTemporary || false);
|
||||||
res.json({ message: "Password updated successfully" });
|
res.json({ message: "Password updated successfully" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -658,7 +664,31 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get user active status
|
// Get user status (for frontend to check password status without exposing passwords)
|
||||||
|
app.get('/api/users/:id/status', isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.claims.sub;
|
||||||
|
const targetUserId = req.params.id;
|
||||||
|
const user = await storage.getUser(userId);
|
||||||
|
|
||||||
|
// Allow users to check their own status or admins to check any status
|
||||||
|
if (userId !== targetUserId && user?.role !== "admin") {
|
||||||
|
return res.status(403).json({ message: "Permission denied" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await storage.getUserStatus(targetUserId);
|
||||||
|
res.json({
|
||||||
|
isActive: status.derivedActive,
|
||||||
|
hasPassword: status.hasPassword,
|
||||||
|
needsPasswordChange: status.needsPasswordChange
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting user status:", error);
|
||||||
|
res.status(500).json({ message: "Failed to get user status" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get user active status (admin only)
|
||||||
app.get('/api/users/:id/active-status', isAuthenticated, isAdmin, async (req, res) => {
|
app.get('/api/users/:id/active-status', isAuthenticated, isAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const isActive = await storage.getUserActiveStatus(req.params.id);
|
const isActive = await storage.getUserActiveStatus(req.params.id);
|
||||||
|
|||||||
+104
-26
@@ -38,15 +38,40 @@ import {
|
|||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { eq, and, gte, lte, desc, asc, sql, inArray } from "drizzle-orm";
|
import { eq, and, gte, lte, desc, asc, sql, inArray } from "drizzle-orm";
|
||||||
|
import * as bcrypt from "bcrypt";
|
||||||
|
|
||||||
|
// Password hashing utilities
|
||||||
|
const SALT_ROUNDS = 12;
|
||||||
|
|
||||||
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
|
return await bcrypt.hash(password, SALT_ROUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
|
||||||
|
return await bcrypt.compare(password, hashedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe user type without password fields
|
||||||
|
export type SafeUser = Omit<User, 'password' | 'tempPassword'>;
|
||||||
|
|
||||||
|
// Helper function to remove password fields from user objects
|
||||||
|
export function sanitizeUser(user: User): SafeUser {
|
||||||
|
const { password, tempPassword, ...safeUser } = user;
|
||||||
|
return safeUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeUsers(users: User[]): SafeUser[] {
|
||||||
|
return users.map(sanitizeUser);
|
||||||
|
}
|
||||||
|
|
||||||
export interface IStorage {
|
export interface IStorage {
|
||||||
// User operations (IMPORTANT: mandatory for Replit Auth)
|
// User operations (IMPORTANT: mandatory for Replit Auth)
|
||||||
getUser(id: string): Promise<User | undefined>;
|
getUser(id: string): Promise<SafeUser | undefined>;
|
||||||
getUserByEmail(email: string): Promise<User | undefined>;
|
getUserByEmail(email: string): Promise<SafeUser | undefined>;
|
||||||
upsertUser(user: UpsertUser): Promise<User>;
|
upsertUser(user: UpsertUser): Promise<SafeUser>;
|
||||||
createUser(user: InsertUser): Promise<User>;
|
createUser(user: InsertUser): Promise<SafeUser>;
|
||||||
updateUser(id: string, updates: UpdateUser): Promise<User>;
|
updateUser(id: string, updates: UpdateUser): Promise<SafeUser>;
|
||||||
getAllDJs(): Promise<User[]>;
|
getAllDJs(): Promise<(SafeUser & { derivedActive: boolean })[]>;
|
||||||
deactivateUser(id: string): Promise<void>;
|
deactivateUser(id: string): Promise<void>;
|
||||||
reactivateUser(id: string): Promise<void>;
|
reactivateUser(id: string): Promise<void>;
|
||||||
makeAdmin(id: string): Promise<void>;
|
makeAdmin(id: string): Promise<void>;
|
||||||
@@ -55,7 +80,9 @@ export interface IStorage {
|
|||||||
deactivateUserSoft(id: string): Promise<void>;
|
deactivateUserSoft(id: string): Promise<void>;
|
||||||
deleteUserHard(id: string): Promise<void>;
|
deleteUserHard(id: string): Promise<void>;
|
||||||
getUserActiveStatus(id: string): Promise<boolean>;
|
getUserActiveStatus(id: string): Promise<boolean>;
|
||||||
createUserWithPassword(userData: InsertUser & { password: string, isTemporary?: boolean }): Promise<User>;
|
getUserStatus(id: string): Promise<{ hasPassword: boolean; isActiveFlag: boolean; derivedActive: boolean; needsPasswordChange: boolean }>;
|
||||||
|
createUserWithPassword(userData: InsertUser & { password: string, isTemporary?: boolean }): Promise<SafeUser>;
|
||||||
|
verifyUserPassword(id: string, password: string): Promise<boolean>;
|
||||||
|
|
||||||
// Event operations
|
// Event operations
|
||||||
createEvent(event: InsertEvent): Promise<Event>;
|
createEvent(event: InsertEvent): Promise<Event>;
|
||||||
@@ -139,34 +166,35 @@ export interface IStorage {
|
|||||||
|
|
||||||
export class DatabaseStorage implements IStorage {
|
export class DatabaseStorage implements IStorage {
|
||||||
// User operations
|
// User operations
|
||||||
async getUser(id: string): Promise<User | undefined> {
|
async getUser(id: string): Promise<SafeUser | undefined> {
|
||||||
const [user] = await db.select().from(users).where(eq(users.id, id));
|
const [user] = await db.select().from(users).where(eq(users.id, id));
|
||||||
return user;
|
return user ? sanitizeUser(user) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByEmail(email: string): Promise<User | undefined> {
|
async getUserByEmail(email: string): Promise<SafeUser | undefined> {
|
||||||
const [user] = await db.select().from(users).where(eq(users.email, email));
|
const [user] = await db.select().from(users).where(eq(users.email, email));
|
||||||
return user;
|
return user ? sanitizeUser(user) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUser(userData: InsertUser): Promise<User> {
|
async createUser(userData: InsertUser): Promise<SafeUser> {
|
||||||
const [user] = await db.insert(users).values(userData).returning();
|
const [user] = await db.insert(users).values(userData).returning();
|
||||||
return user;
|
return sanitizeUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUserWithPassword(userData: InsertUser & { password: string, isTemporary?: boolean }): Promise<User> {
|
async createUserWithPassword(userData: InsertUser & { password: string, isTemporary?: boolean }): Promise<SafeUser> {
|
||||||
const { password, isTemporary, ...userFields } = userData;
|
const { password, isTemporary, ...userFields } = userData;
|
||||||
|
const hashedPassword = await hashPassword(password);
|
||||||
const [user] = await db.insert(users).values({
|
const [user] = await db.insert(users).values({
|
||||||
...userFields,
|
...userFields,
|
||||||
password,
|
password: hashedPassword,
|
||||||
tempPassword: isTemporary ? password : null,
|
tempPassword: isTemporary ? hashedPassword : null,
|
||||||
needsPasswordChange: isTemporary || false,
|
needsPasswordChange: isTemporary || false,
|
||||||
isActive: true // Activate immediately when password is provided
|
isActive: true // Activate immediately when password is provided
|
||||||
}).returning();
|
}).returning();
|
||||||
return user;
|
return sanitizeUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertUser(userData: UpsertUser): Promise<User> {
|
async upsertUser(userData: UpsertUser): Promise<SafeUser> {
|
||||||
const [user] = await db
|
const [user] = await db
|
||||||
.insert(users)
|
.insert(users)
|
||||||
.values(userData)
|
.values(userData)
|
||||||
@@ -178,20 +206,29 @@ export class DatabaseStorage implements IStorage {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
return user;
|
return sanitizeUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser(id: string, updates: UpdateUser): Promise<User> {
|
async updateUser(id: string, updates: UpdateUser): Promise<SafeUser> {
|
||||||
const [user] = await db
|
const [user] = await db
|
||||||
.update(users)
|
.update(users)
|
||||||
.set({ ...updates, updatedAt: new Date() })
|
.set({ ...updates, updatedAt: new Date() })
|
||||||
.where(eq(users.id, id))
|
.where(eq(users.id, id))
|
||||||
.returning();
|
.returning();
|
||||||
return user;
|
return sanitizeUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllDJs(): Promise<User[]> {
|
async getAllDJs(): Promise<(SafeUser & { derivedActive: boolean })[]> {
|
||||||
return await db.select().from(users).where(eq(users.role, "dj")).orderBy(asc(users.displayName));
|
const users_list = await db.select().from(users).where(eq(users.role, "dj")).orderBy(asc(users.displayName));
|
||||||
|
|
||||||
|
// Add derived active status based on password presence while sanitizing
|
||||||
|
return users_list.map(user => {
|
||||||
|
const sanitizedUser = sanitizeUser(user);
|
||||||
|
return {
|
||||||
|
...sanitizedUser,
|
||||||
|
derivedActive: user.isActive && !!user.password
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deactivateUser(id: string): Promise<void> {
|
async deactivateUser(id: string): Promise<void> {
|
||||||
@@ -211,9 +248,10 @@ export class DatabaseStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setUserPassword(id: string, password: string, isTemporary: boolean = false): Promise<void> {
|
async setUserPassword(id: string, password: string, isTemporary: boolean = false): Promise<void> {
|
||||||
|
const hashedPassword = await hashPassword(password);
|
||||||
await db.update(users).set({
|
await db.update(users).set({
|
||||||
password,
|
password: hashedPassword,
|
||||||
tempPassword: isTemporary ? password : null,
|
tempPassword: isTemporary ? hashedPassword : null,
|
||||||
needsPasswordChange: isTemporary,
|
needsPasswordChange: isTemporary,
|
||||||
isActive: true, // Activate user when password is set
|
isActive: true, // Activate user when password is set
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
@@ -222,7 +260,14 @@ export class DatabaseStorage implements IStorage {
|
|||||||
|
|
||||||
async deactivateUserSoft(id: string): Promise<void> {
|
async deactivateUserSoft(id: string): Promise<void> {
|
||||||
// Soft deactivation - hide from availability/scheduling but keep all data
|
// Soft deactivation - hide from availability/scheduling but keep all data
|
||||||
await db.update(users).set({ isActive: false, updatedAt: new Date() }).where(eq(users.id, id));
|
console.log(`[DEBUG] Attempting to deactivate user with id: ${id}`);
|
||||||
|
|
||||||
|
const result = await db.update(users).set({ isActive: false, updatedAt: new Date() }).where(eq(users.id, id));
|
||||||
|
console.log(`[DEBUG] Deactivation result:`, result);
|
||||||
|
|
||||||
|
// Verify the update by reading the user back
|
||||||
|
const [updatedUser] = await db.select({ id: users.id, isActive: users.isActive }).from(users).where(eq(users.id, id));
|
||||||
|
console.log(`[DEBUG] User after update:`, updatedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUserHard(id: string): Promise<void> {
|
async deleteUserHard(id: string): Promise<void> {
|
||||||
@@ -259,6 +304,39 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return user ? user.isActive && !!user.password : false;
|
return user ? user.isActive && !!user.password : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserStatus(id: string): Promise<{ hasPassword: boolean; isActiveFlag: boolean; derivedActive: boolean; needsPasswordChange: boolean }> {
|
||||||
|
const [user] = await db.select({
|
||||||
|
isActive: users.isActive,
|
||||||
|
password: users.password,
|
||||||
|
needsPasswordChange: users.needsPasswordChange
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, id));
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return { hasPassword: false, isActiveFlag: false, derivedActive: false, needsPasswordChange: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasPassword = !!user.password;
|
||||||
|
const isActiveFlag = user.isActive;
|
||||||
|
const derivedActive = isActiveFlag && hasPassword;
|
||||||
|
const needsPasswordChange = user.needsPasswordChange || false;
|
||||||
|
|
||||||
|
return { hasPassword, isActiveFlag, derivedActive, needsPasswordChange };
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyUserPassword(id: string, password: string): Promise<boolean> {
|
||||||
|
const [user] = await db.select({ password: users.password })
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, id));
|
||||||
|
|
||||||
|
if (!user?.password) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await verifyPassword(password, user.password);
|
||||||
|
}
|
||||||
|
|
||||||
// Event operations
|
// Event operations
|
||||||
async createEvent(event: InsertEvent): Promise<Event> {
|
async createEvent(event: InsertEvent): Promise<Event> {
|
||||||
const [newEvent] = await db.insert(events).values(event).returning();
|
const [newEvent] = await db.insert(events).values(event).returning();
|
||||||
|
|||||||
Reference in New Issue
Block a user