Provide admin tools to manage and resend DJ invitations easily
Implements invitation management features, including resending and manual user creation, via new API endpoints and React components. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 3a22ac80-cd1d-4441-9e36-f24fc2f4c3de Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3478f7c3-db8c-4fca-9165-3adbdf1b5829/dbb6f90a-b277-4de1-a273-07d2fc2d56d1.jpg
This commit is contained in:
@@ -10,6 +10,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
|||||||
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 } from "lucide-react";
|
||||||
|
import { InvitationManagement } from "./InvitationManagement";
|
||||||
|
|
||||||
export function DJManagement() {
|
export function DJManagement() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -238,26 +239,14 @@ export function DJManagement() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{invitations && invitations.length > 0 && (
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Pending Invitations</CardTitle>
|
<CardTitle>Invitation Management</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<InvitationManagement invitations={invitations || []} />
|
||||||
{invitations.map((invitation: any) => (
|
|
||||||
<div key={invitation.id} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{invitation.name}</div>
|
|
||||||
<div className="text-sm text-slate-500">{invitation.email}</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant="secondary">Pending</Badge>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,333 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { Mail, UserPlus, RotateCcw, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface Invitation {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
token: string;
|
||||||
|
expiresAt: string;
|
||||||
|
isUsed: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InvitationManagementProps {
|
||||||
|
invitations: Invitation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InvitationManagement({ invitations }: InvitationManagementProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [isResendOpen, setIsResendOpen] = useState(false);
|
||||||
|
const [isManualCreateOpen, setIsManualCreateOpen] = useState(false);
|
||||||
|
const [selectedInvitation, setSelectedInvitation] = useState<Invitation | null>(null);
|
||||||
|
|
||||||
|
const [manualUserData, setManualUserData] = useState({
|
||||||
|
email: "",
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
displayName: "",
|
||||||
|
tempPassword: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resend invitation mutation
|
||||||
|
const resendInvitationMutation = useMutation({
|
||||||
|
mutationFn: async (invitationId: number) => {
|
||||||
|
await apiRequest(`/api/invitations/${invitationId}/resend`, {
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Invitation resent successfully",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/invitations'] });
|
||||||
|
setIsResendOpen(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: `Failed to resend invitation: ${error.message}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete invitation mutation
|
||||||
|
const deleteInvitationMutation = useMutation({
|
||||||
|
mutationFn: async (invitationId: number) => {
|
||||||
|
await apiRequest(`/api/invitations/${invitationId}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Invitation deleted successfully",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/invitations'] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: `Failed to delete invitation: ${error.message}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual user creation mutation
|
||||||
|
const createUserMutation = useMutation({
|
||||||
|
mutationFn: async (userData: typeof manualUserData) => {
|
||||||
|
await apiRequest("/api/users/create-manual", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(userData),
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "User created successfully. They can now log in with their temporary password.",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/users'] });
|
||||||
|
setIsManualCreateOpen(false);
|
||||||
|
setManualUserData({
|
||||||
|
email: "",
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
displayName: "",
|
||||||
|
tempPassword: ""
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: `Failed to create user: ${error.message}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResend = (invitation: Invitation) => {
|
||||||
|
setSelectedInvitation(invitation);
|
||||||
|
setIsResendOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (invitation: Invitation) => {
|
||||||
|
if (window.confirm(`Are you sure you want to delete the invitation for ${invitation.email}?`)) {
|
||||||
|
deleteInvitationMutation.mutate(invitation.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleManualCreate = () => {
|
||||||
|
setIsManualCreateOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateTempPassword = () => {
|
||||||
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
let password = "";
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
setManualUserData(prev => ({ ...prev, tempPassword: password }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExpired = (expiresAt: string) => {
|
||||||
|
return new Date(expiresAt) < new Date();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-semibold">Invitation Management</h3>
|
||||||
|
<Button onClick={handleManualCreate} className="bg-green-600 hover:bg-green-700">
|
||||||
|
<UserPlus className="w-4 h-4 mr-2" />
|
||||||
|
Manual Activate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{invitations.map((invitation) => (
|
||||||
|
<div
|
||||||
|
key={invitation.id}
|
||||||
|
className={`p-4 border rounded-lg ${
|
||||||
|
invitation.isUsed
|
||||||
|
? 'bg-green-50 border-green-200'
|
||||||
|
: isExpired(invitation.expiresAt)
|
||||||
|
? 'bg-red-50 border-red-200'
|
||||||
|
: 'bg-gray-50 border-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Mail className="w-4 h-4 text-gray-500" />
|
||||||
|
<span className="font-medium">{invitation.email}</span>
|
||||||
|
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||||
|
invitation.isUsed
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: isExpired(invitation.expiresAt)
|
||||||
|
? 'bg-red-100 text-red-800'
|
||||||
|
: 'bg-yellow-100 text-yellow-800'
|
||||||
|
}`}>
|
||||||
|
{invitation.isUsed ? 'Used' : isExpired(invitation.expiresAt) ? 'Expired' : 'Pending'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
<p>Created: {new Date(invitation.createdAt).toLocaleDateString()}</p>
|
||||||
|
<p>Expires: {new Date(invitation.expiresAt).toLocaleDateString()}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!invitation.isUsed && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleResend(invitation)}
|
||||||
|
disabled={resendInvitationMutation.isPending}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-1" />
|
||||||
|
Resend
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(invitation)}
|
||||||
|
disabled={deleteInvitationMutation.isPending}
|
||||||
|
className="text-red-600 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-1" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Resend Confirmation Dialog */}
|
||||||
|
<Dialog open={isResendOpen} onOpenChange={setIsResendOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Resend Invitation</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<p>Are you sure you want to resend the invitation to {selectedInvitation?.email}?</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
This will generate a new invitation link and extend the expiration date by 7 days.
|
||||||
|
</p>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsResendOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => selectedInvitation && resendInvitationMutation.mutate(selectedInvitation.id)}
|
||||||
|
disabled={resendInvitationMutation.isPending}
|
||||||
|
>
|
||||||
|
{resendInvitationMutation.isPending ? "Sending..." : "Resend"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Manual User Creation Dialog */}
|
||||||
|
<Dialog open={isManualCreateOpen} onOpenChange={setIsManualCreateOpen}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Manual User Activation</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="email">Email Address *</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={manualUserData.email}
|
||||||
|
onChange={(e) => setManualUserData(prev => ({ ...prev, email: e.target.value }))}
|
||||||
|
placeholder="dj@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="firstName">First Name</Label>
|
||||||
|
<Input
|
||||||
|
id="firstName"
|
||||||
|
value={manualUserData.firstName}
|
||||||
|
onChange={(e) => setManualUserData(prev => ({ ...prev, firstName: e.target.value }))}
|
||||||
|
placeholder="John"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="lastName">Last Name</Label>
|
||||||
|
<Input
|
||||||
|
id="lastName"
|
||||||
|
value={manualUserData.lastName}
|
||||||
|
onChange={(e) => setManualUserData(prev => ({ ...prev, lastName: e.target.value }))}
|
||||||
|
placeholder="Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="displayName">Display Name</Label>
|
||||||
|
<Input
|
||||||
|
id="displayName"
|
||||||
|
value={manualUserData.displayName}
|
||||||
|
onChange={(e) => setManualUserData(prev => ({ ...prev, displayName: e.target.value }))}
|
||||||
|
placeholder="DJ John"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="tempPassword">Temporary Password *</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="tempPassword"
|
||||||
|
type="text"
|
||||||
|
value={manualUserData.tempPassword}
|
||||||
|
onChange={(e) => setManualUserData(prev => ({ ...prev, tempPassword: e.target.value }))}
|
||||||
|
placeholder="Enter temporary password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={generateTempPassword}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
The user will need to change this password on first login.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsManualCreateOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => createUserMutation.mutate(manualUserData)}
|
||||||
|
disabled={createUserMutation.isPending || !manualUserData.email || !manualUserData.tempPassword}
|
||||||
|
>
|
||||||
|
{createUserMutation.isPending ? "Creating..." : "Create User"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -539,6 +539,90 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Resend invitation
|
||||||
|
app.post('/api/invitations/:id/resend', isAuthenticated, isAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const invitationId = parseInt(req.params.id);
|
||||||
|
const invitation = await storage.getInvitation(invitationId);
|
||||||
|
|
||||||
|
if (!invitation) {
|
||||||
|
return res.status(404).json({ message: "Invitation not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new token and extend expiration
|
||||||
|
const newToken = crypto.randomBytes(32).toString('hex');
|
||||||
|
const newExpiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
||||||
|
|
||||||
|
const updatedInvitation = await storage.updateInvitation(invitationId, {
|
||||||
|
token: newToken,
|
||||||
|
expiresAt: newExpiresAt,
|
||||||
|
isUsed: false
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "Invitation resent successfully",
|
||||||
|
invitation: updatedInvitation
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error resending invitation:", error);
|
||||||
|
res.status(500).json({ message: "Failed to resend invitation" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete invitation
|
||||||
|
app.delete('/api/invitations/:id', isAuthenticated, isAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const invitationId = parseInt(req.params.id);
|
||||||
|
await storage.deleteInvitation(invitationId);
|
||||||
|
res.json({ message: "Invitation deleted successfully" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting invitation:", error);
|
||||||
|
res.status(500).json({ message: "Failed to delete invitation" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual activation - create user directly
|
||||||
|
app.post('/api/users/create-manual', isAuthenticated, isAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, firstName, lastName, displayName, tempPassword } = req.body;
|
||||||
|
|
||||||
|
if (!email || !tempPassword) {
|
||||||
|
return res.status(400).json({ message: "Email and temporary password are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
const existingUser = await storage.getUserByEmail(email);
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(409).json({ message: "User with this email already exists" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user with temporary ID (will be replaced when they log in via Replit Auth)
|
||||||
|
const tempUserId = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
id: tempUserId,
|
||||||
|
email,
|
||||||
|
firstName: firstName || '',
|
||||||
|
lastName: lastName || '',
|
||||||
|
displayName: displayName || `${firstName || ''} ${lastName || ''}`.trim(),
|
||||||
|
role: 'dj' as const,
|
||||||
|
isActive: true,
|
||||||
|
tempPassword, // Store temporarily - will be removed after first login
|
||||||
|
needsPasswordChange: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = await storage.createUser(userData);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: "User created successfully",
|
||||||
|
user: { ...user, tempPassword: undefined } // Don't send password back
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating user manually:", error);
|
||||||
|
res.status(500).json({ message: "Failed to create user manually" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Statistics routes
|
// Statistics routes
|
||||||
app.get('/api/stats/dashboard', isAuthenticated, async (req: any, res) => {
|
app.get('/api/stats/dashboard', isAuthenticated, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ import { eq, and, gte, lte, desc, asc, sql, inArray } from "drizzle-orm";
|
|||||||
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<User | undefined>;
|
||||||
|
getUserByEmail(email: string): Promise<User | undefined>;
|
||||||
upsertUser(user: UpsertUser): Promise<User>;
|
upsertUser(user: UpsertUser): Promise<User>;
|
||||||
|
createUser(user: InsertUser): Promise<User>;
|
||||||
updateUser(id: string, updates: UpdateUser): Promise<User>;
|
updateUser(id: string, updates: UpdateUser): Promise<User>;
|
||||||
getAllDJs(): Promise<User[]>;
|
getAllDJs(): Promise<User[]>;
|
||||||
deactivateUser(id: string): Promise<void>;
|
deactivateUser(id: string): Promise<void>;
|
||||||
@@ -108,7 +110,10 @@ export interface IStorage {
|
|||||||
|
|
||||||
// Invitation operations
|
// Invitation operations
|
||||||
createInvitation(invitation: InsertInvitation): Promise<Invitation>;
|
createInvitation(invitation: InsertInvitation): Promise<Invitation>;
|
||||||
|
getInvitation(id: number): Promise<Invitation | undefined>;
|
||||||
getInvitationByToken(token: string): Promise<Invitation | undefined>;
|
getInvitationByToken(token: string): Promise<Invitation | undefined>;
|
||||||
|
updateInvitation(id: number, updates: Partial<InsertInvitation>): Promise<Invitation>;
|
||||||
|
deleteInvitation(id: number): Promise<void>;
|
||||||
markInvitationAsUsed(id: number): Promise<void>;
|
markInvitationAsUsed(id: number): Promise<void>;
|
||||||
getActiveInvitations(): Promise<Invitation[]>;
|
getActiveInvitations(): Promise<Invitation[]>;
|
||||||
|
|
||||||
@@ -134,6 +139,16 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserByEmail(email: string): Promise<User | undefined> {
|
||||||
|
const [user] = await db.select().from(users).where(eq(users.email, email));
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(userData: InsertUser): Promise<User> {
|
||||||
|
const [user] = await db.insert(users).values(userData).returning();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
async upsertUser(userData: UpsertUser): Promise<User> {
|
async upsertUser(userData: UpsertUser): Promise<User> {
|
||||||
const [user] = await db
|
const [user] = await db
|
||||||
.insert(users)
|
.insert(users)
|
||||||
@@ -450,11 +465,29 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return newInvitation;
|
return newInvitation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getInvitation(id: number): Promise<Invitation | undefined> {
|
||||||
|
const [invitation] = await db.select().from(invitations).where(eq(invitations.id, id));
|
||||||
|
return invitation;
|
||||||
|
}
|
||||||
|
|
||||||
async getInvitationByToken(token: string): Promise<Invitation | undefined> {
|
async getInvitationByToken(token: string): Promise<Invitation | undefined> {
|
||||||
const [invitation] = await db.select().from(invitations).where(eq(invitations.token, token));
|
const [invitation] = await db.select().from(invitations).where(eq(invitations.token, token));
|
||||||
return invitation;
|
return invitation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateInvitation(id: number, updates: Partial<InsertInvitation>): Promise<Invitation> {
|
||||||
|
const [invitation] = await db
|
||||||
|
.update(invitations)
|
||||||
|
.set(updates)
|
||||||
|
.where(eq(invitations.id, id))
|
||||||
|
.returning();
|
||||||
|
return invitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteInvitation(id: number): Promise<void> {
|
||||||
|
await db.delete(invitations).where(eq(invitations.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
async markInvitationAsUsed(id: number): Promise<void> {
|
async markInvitationAsUsed(id: number): Promise<void> {
|
||||||
await db.update(invitations).set({ isUsed: true }).where(eq(invitations.id, id));
|
await db.update(invitations).set({ isUsed: true }).where(eq(invitations.id, id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export const users = pgTable("users", {
|
|||||||
role: userRoleEnum("role").default("dj").notNull(),
|
role: userRoleEnum("role").default("dj").notNull(),
|
||||||
isActive: boolean("is_active").default(true).notNull(),
|
isActive: boolean("is_active").default(true).notNull(),
|
||||||
maxEventsPerMonth: integer("max_events_per_month").default(2).notNull(),
|
maxEventsPerMonth: integer("max_events_per_month").default(2).notNull(),
|
||||||
|
tempPassword: varchar("temp_password"),
|
||||||
|
needsPasswordChange: boolean("needs_password_change").default(false),
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user