Files
DJ-Management-Tool/server/routes.ts
T
spliceboti 60b5735588 Add user management for activation, deactivation, and deletion
Introduce API endpoints and storage methods for setting user passwords, soft deactivation (hiding availability), and hard deletion (removing all associated future data).

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 3a22ac80-cd1d-4441-9e36-f24fc2f4c3de
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3478f7c3-db8c-4fca-9165-3adbdf1b5829/3a22ac80-cd1d-4441-9e36-f24fc2f4c3de/gBqmpbl
2025-09-12 20:36:14 +00:00

697 lines
25 KiB
TypeScript

import type { Express } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import { setupAuth, isAuthenticated } from "./replitAuth";
import {
insertEventSchema,
insertEventTypeSchema,
insertScheduleTemplateSchema,
insertScheduleTemplateSlotSchema,
insertSocialLinkSchema,
insertAvailabilitySchema,
insertRemovalRequestSchema,
insertInvitationSchema,
updateUserSchema,
updateEventSchema,
updateEventTypeSchema,
updateScheduleTemplateSchema,
updateRemovalRequestSchema,
} from "@shared/schema";
import { z } from "zod";
import { fromZodError } from "zod-validation-error";
import crypto from "crypto";
export async function registerRoutes(app: Express): Promise<Server> {
// Auth middleware
await setupAuth(app);
// Helper function to check if user is admin
const isAdmin = async (req: any, res: any, next: any) => {
try {
const userId = req.user.claims.sub;
const user = await storage.getUser(userId);
if (!user || user.role !== "admin") {
return res.status(403).json({ message: "Admin access required" });
}
next();
} catch (error) {
res.status(500).json({ message: "Error checking admin status" });
}
};
// Auth routes
app.get('/api/auth/user', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const user = await storage.getUser(userId);
res.json(user);
} catch (error) {
console.error("Error fetching user:", error);
res.status(500).json({ message: "Failed to fetch user" });
}
});
// User management routes
app.get('/api/users', isAuthenticated, isAdmin, async (req, res) => {
try {
const users = await storage.getAllDJs();
res.json(users);
} catch (error) {
console.error("Error fetching users:", error);
res.status(500).json({ message: "Failed to fetch users" });
}
});
app.patch('/api/users/:id', 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 update their own profile or admins to update any profile
if (userId !== targetUserId && user?.role !== "admin") {
return res.status(403).json({ message: "Permission denied" });
}
const updates = updateUserSchema.parse(req.body);
const updatedUser = await storage.updateUser(targetUserId, updates);
res.json(updatedUser);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error updating user:", error);
res.status(500).json({ message: "Failed to update user" });
}
});
app.post('/api/users/:id/deactivate', isAuthenticated, isAdmin, async (req, res) => {
try {
await storage.deactivateUser(req.params.id);
res.json({ message: "User deactivated successfully" });
} catch (error) {
console.error("Error deactivating user:", error);
res.status(500).json({ message: "Failed to deactivate user" });
}
});
app.post('/api/users/:id/reactivate', isAuthenticated, isAdmin, async (req, res) => {
try {
await storage.reactivateUser(req.params.id);
res.json({ message: "User reactivated successfully" });
} catch (error) {
console.error("Error reactivating user:", error);
res.status(500).json({ message: "Failed to reactivate user" });
}
});
app.post('/api/users/:id/make-admin', isAuthenticated, isAdmin, async (req, res) => {
try {
await storage.makeAdmin(req.params.id);
res.json({ message: "User promoted to admin successfully" });
} catch (error) {
console.error("Error promoting user to admin:", error);
res.status(500).json({ message: "Failed to promote user to admin" });
}
});
app.post('/api/users/:id/remove-admin', isAuthenticated, isAdmin, async (req, res) => {
try {
await storage.removeAdmin(req.params.id);
res.json({ message: "Admin privileges removed successfully" });
} catch (error) {
console.error("Error removing admin privileges:", error);
res.status(500).json({ message: "Failed to remove admin privileges" });
}
});
// Event routes
app.get('/api/events', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const user = await storage.getUser(userId);
if (user?.role === "admin") {
const events = await storage.getAllEvents();
res.json(events);
} else {
const events = await storage.getEventsByDJ(userId);
res.json(events);
}
} catch (error) {
console.error("Error fetching events:", error);
res.status(500).json({ message: "Failed to fetch events" });
}
});
app.get('/api/events/upcoming', isAuthenticated, async (req, res) => {
try {
const limit = req.query.limit ? parseInt(req.query.limit as string) : 50;
const events = await storage.getUpcomingEvents(limit);
res.json(events);
} catch (error) {
console.error("Error fetching upcoming events:", error);
res.status(500).json({ message: "Failed to fetch upcoming events" });
}
});
app.post('/api/events', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const eventData = insertEventSchema.parse({ ...req.body, djId: userId });
const event = await storage.createEvent(eventData);
res.json(event);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error creating event:", error);
res.status(500).json({ message: "Failed to create event" });
}
});
app.get('/api/events/:id', isAuthenticated, async (req: any, res) => {
try {
const event = await storage.getEvent(parseInt(req.params.id));
if (!event) {
return res.status(404).json({ message: "Event not found" });
}
const userId = req.user.claims.sub;
const user = await storage.getUser(userId);
// Allow access to event if user is admin or the DJ assigned to the event
if (user?.role !== "admin" && event.djId !== userId) {
return res.status(403).json({ message: "Permission denied" });
}
res.json(event);
} catch (error) {
console.error("Error fetching event:", error);
res.status(500).json({ message: "Failed to fetch event" });
}
});
app.patch('/api/events/:id', isAuthenticated, async (req: any, res) => {
try {
const eventId = parseInt(req.params.id);
const event = await storage.getEvent(eventId);
if (!event) {
return res.status(404).json({ message: "Event not found" });
}
const userId = req.user.claims.sub;
const user = await storage.getUser(userId);
// Only allow DJ to edit their own self-added events or admin to edit any event
if (user?.role !== "admin" && (event.djId !== userId || event.isAssignedByAdmin)) {
return res.status(403).json({ message: "Permission denied" });
}
const updates = updateEventSchema.parse(req.body);
const updatedEvent = await storage.updateEvent(eventId, updates);
res.json(updatedEvent);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error updating event:", error);
res.status(500).json({ message: "Failed to update event" });
}
});
app.delete('/api/events/:id', isAuthenticated, async (req: any, res) => {
try {
const eventId = parseInt(req.params.id);
const event = await storage.getEvent(eventId);
if (!event) {
return res.status(404).json({ message: "Event not found" });
}
const userId = req.user.claims.sub;
const user = await storage.getUser(userId);
// Only allow DJ to delete their own self-added events or admin to delete any event
if (user?.role !== "admin" && (event.djId !== userId || event.isAssignedByAdmin)) {
return res.status(403).json({ message: "Permission denied" });
}
await storage.deleteEvent(eventId);
res.json({ message: "Event deleted successfully" });
} catch (error) {
console.error("Error deleting event:", error);
res.status(500).json({ message: "Failed to delete event" });
}
});
// Public API for WordPress integration
app.get('/api/public/upcoming-events', async (req, res) => {
try {
const limit = req.query.limit ? parseInt(req.query.limit as string) : 10;
const events = await storage.getUpcomingEvents(limit);
// Format events for WordPress widget
const formattedEvents = events.map(event => ({
id: event.id,
name: event.name,
date: event.date,
startTime: event.startTime,
endTime: event.endTime,
locationName: event.locationName,
locationAddress: event.locationAddress,
djId: event.djId,
eventTypeId: event.eventTypeId,
description: event.description,
}));
res.json(formattedEvents);
} catch (error) {
console.error("Error fetching public events:", error);
res.status(500).json({ message: "Failed to fetch events" });
}
});
// Event type routes
app.get('/api/event-types', isAuthenticated, async (req, res) => {
try {
const eventTypes = await storage.getAllEventTypes();
res.json(eventTypes);
} catch (error) {
console.error("Error fetching event types:", error);
res.status(500).json({ message: "Failed to fetch event types" });
}
});
app.post('/api/event-types', isAuthenticated, isAdmin, async (req, res) => {
try {
const eventTypeData = insertEventTypeSchema.parse(req.body);
const eventType = await storage.createEventType(eventTypeData);
res.json(eventType);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error creating event type:", error);
res.status(500).json({ message: "Failed to create event type" });
}
});
app.patch('/api/event-types/:id', isAuthenticated, isAdmin, async (req, res) => {
try {
const eventTypeId = parseInt(req.params.id);
const updates = updateEventTypeSchema.parse(req.body);
const eventType = await storage.updateEventType(eventTypeId, updates);
res.json(eventType);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error updating event type:", error);
res.status(500).json({ message: "Failed to update event type" });
}
});
app.delete('/api/event-types/:id', isAuthenticated, isAdmin, async (req, res) => {
try {
const eventTypeId = parseInt(req.params.id);
await storage.deleteEventType(eventTypeId);
res.json({ message: "Event type deleted successfully" });
} catch (error) {
console.error("Error deleting event type:", error);
res.status(500).json({ message: "Failed to delete event type" });
}
});
// Schedule template routes
app.get('/api/schedule-templates', isAuthenticated, isAdmin, async (req, res) => {
try {
const templates = await storage.getAllScheduleTemplates();
res.json(templates);
} catch (error) {
console.error("Error fetching schedule templates:", error);
res.status(500).json({ message: "Failed to fetch schedule templates" });
}
});
app.post('/api/schedule-templates', isAuthenticated, isAdmin, async (req, res) => {
try {
const templateData = insertScheduleTemplateSchema.parse(req.body);
const template = await storage.createScheduleTemplate(templateData);
res.json(template);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error creating schedule template:", error);
res.status(500).json({ message: "Failed to create schedule template" });
}
});
app.get('/api/schedule-templates/:id/slots', isAuthenticated, isAdmin, async (req, res) => {
try {
const templateId = parseInt(req.params.id);
const slots = await storage.getScheduleTemplateSlots(templateId);
res.json(slots);
} catch (error) {
console.error("Error fetching template slots:", error);
res.status(500).json({ message: "Failed to fetch template slots" });
}
});
app.post('/api/schedule-templates/:id/slots', isAuthenticated, isAdmin, async (req, res) => {
try {
const templateId = parseInt(req.params.id);
const slotData = insertScheduleTemplateSlotSchema.parse({ ...req.body, templateId });
const slot = await storage.createScheduleTemplateSlot(slotData);
res.json(slot);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error creating template slot:", error);
res.status(500).json({ message: "Failed to create template slot" });
}
});
// Social links routes
app.get('/api/social-links', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const links = await storage.getSocialLinksByDJ(userId);
res.json(links);
} catch (error) {
console.error("Error fetching social links:", error);
res.status(500).json({ message: "Failed to fetch social links" });
}
});
app.post('/api/social-links', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const linkData = insertSocialLinkSchema.parse({ ...req.body, djId: userId });
const link = await storage.createSocialLink(linkData);
res.json(link);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error creating social link:", error);
res.status(500).json({ message: "Failed to create social link" });
}
});
app.delete('/api/social-links/:id', isAuthenticated, async (req: any, res) => {
try {
await storage.deleteSocialLink(parseInt(req.params.id));
res.json({ message: "Social link deleted successfully" });
} catch (error) {
console.error("Error deleting social link:", error);
res.status(500).json({ message: "Failed to delete social link" });
}
});
// Availability routes
app.get('/api/availability', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const availability = await storage.getAvailabilityByDJ(userId);
res.json(availability);
} catch (error) {
console.error("Error fetching availability:", error);
res.status(500).json({ message: "Failed to fetch availability" });
}
});
app.post('/api/availability', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const availabilityData = insertAvailabilitySchema.parse({ ...req.body, djId: userId });
const availability = await storage.createAvailability(availabilityData);
res.json(availability);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error creating availability:", error);
res.status(500).json({ message: "Failed to create availability" });
}
});
app.delete('/api/availability/:id', isAuthenticated, async (req: any, res) => {
try {
await storage.deleteAvailability(parseInt(req.params.id));
res.json({ message: "Availability deleted successfully" });
} catch (error) {
console.error("Error deleting availability:", error);
res.status(500).json({ message: "Failed to delete availability" });
}
});
// Removal request routes
app.get('/api/removal-requests', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const user = await storage.getUser(userId);
if (user?.role === "admin") {
const requests = await storage.getPendingRemovalRequests();
res.json(requests);
} else {
return res.status(403).json({ message: "Admin access required" });
}
} catch (error) {
console.error("Error fetching removal requests:", error);
res.status(500).json({ message: "Failed to fetch removal requests" });
}
});
app.post('/api/removal-requests', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const requestData = insertRemovalRequestSchema.parse({ ...req.body, djId: userId });
const request = await storage.createRemovalRequest(requestData);
res.json(request);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error creating removal request:", error);
res.status(500).json({ message: "Failed to create removal request" });
}
});
app.post('/api/removal-requests/:id/approve', isAuthenticated, isAdmin, async (req: any, res) => {
try {
const requestId = parseInt(req.params.id);
const reviewedBy = req.user.claims.sub;
await storage.approveRemovalRequest(requestId, reviewedBy);
res.json({ message: "Removal request approved successfully" });
} catch (error) {
console.error("Error approving removal request:", error);
res.status(500).json({ message: "Failed to approve removal request" });
}
});
app.post('/api/removal-requests/:id/deny', isAuthenticated, isAdmin, async (req: any, res) => {
try {
const requestId = parseInt(req.params.id);
const reviewedBy = req.user.claims.sub;
await storage.denyRemovalRequest(requestId, reviewedBy);
res.json({ message: "Removal request denied successfully" });
} catch (error) {
console.error("Error denying removal request:", error);
res.status(500).json({ message: "Failed to deny removal request" });
}
});
// Invitation routes
app.post('/api/invitations', isAuthenticated, isAdmin, async (req: any, res) => {
try {
const createdBy = req.user.claims.sub;
const token = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
const invitationData = insertInvitationSchema.parse({
...req.body,
token,
createdBy,
expiresAt
});
const invitation = await storage.createInvitation(invitationData);
res.json(invitation);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: fromZodError(error).toString() });
}
console.error("Error creating invitation:", error);
res.status(500).json({ message: "Failed to create invitation" });
}
});
app.get('/api/invitations', isAuthenticated, isAdmin, async (req, res) => {
try {
const invitations = await storage.getActiveInvitations();
res.json(invitations);
} catch (error) {
console.error("Error fetching invitations:", error);
res.status(500).json({ message: "Failed to fetch invitations" });
}
});
// 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(),
password: tempPassword,
isTemporary: true
};
const user = await storage.createUserWithPassword(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" });
}
});
// Set/change user password
app.post('/api/users/:id/set-password', isAuthenticated, isAdmin, async (req, res) => {
try {
const { password, isTemporary } = req.body;
if (!password) {
return res.status(400).json({ message: "Password is required" });
}
await storage.setUserPassword(req.params.id, password, isTemporary || false);
res.json({ message: "Password updated successfully" });
} catch (error) {
console.error("Error setting user password:", error);
res.status(500).json({ message: "Failed to set user password" });
}
});
// Soft deactivate user (hide from scheduling but keep data)
app.post('/api/users/:id/deactivate-soft', isAuthenticated, isAdmin, async (req, res) => {
try {
await storage.deactivateUserSoft(req.params.id);
res.json({ message: "User deactivated successfully (data preserved)" });
} catch (error) {
console.error("Error deactivating user:", error);
res.status(500).json({ message: "Failed to deactivate user" });
}
});
// Hard delete user (remove user and future data)
app.delete('/api/users/:id/delete-hard', isAuthenticated, isAdmin, async (req, res) => {
try {
await storage.deleteUserHard(req.params.id);
res.json({ message: "User and associated future data deleted successfully" });
} catch (error) {
console.error("Error deleting user:", error);
res.status(500).json({ message: "Failed to delete user" });
}
});
// Get user active status
app.get('/api/users/:id/active-status', isAuthenticated, isAdmin, async (req, res) => {
try {
const isActive = await storage.getUserActiveStatus(req.params.id);
res.json({ isActive });
} catch (error) {
console.error("Error checking user active status:", error);
res.status(500).json({ message: "Failed to check user status" });
}
});
// Statistics routes
app.get('/api/stats/dashboard', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const stats = await storage.getDashboardStats(userId);
res.json(stats);
} catch (error) {
console.error("Error fetching dashboard stats:", error);
res.status(500).json({ message: "Failed to fetch dashboard stats" });
}
});
app.get('/api/stats/admin', isAuthenticated, isAdmin, async (req, res) => {
try {
const stats = await storage.getAdminStats();
res.json(stats);
} catch (error) {
console.error("Error fetching admin stats:", error);
res.status(500).json({ message: "Failed to fetch admin stats" });
}
});
const httpServer = createServer(app);
return httpServer;
}