From 60b573558854c8225de7847f51f834b79cf1d03c Mon Sep 17 00:00:00 2001 From: spliceboti <44727389-spliceboti@users.noreply.replit.com> Date: Fri, 12 Sep 2025 20:36:14 +0000 Subject: [PATCH] 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 --- server/routes.ts | 56 ++++++++++++++++++++++++++++++++++++---- server/storage.ts | 66 +++++++++++++++++++++++++++++++++++++++++++++++ shared/schema.ts | 1 + 3 files changed, 118 insertions(+), 5 deletions(-) diff --git a/server/routes.ts b/server/routes.ts index a4ce06e..e4e807b 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -605,13 +605,11 @@ export async function registerRoutes(app: Express): Promise { 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 + password: tempPassword, + isTemporary: true }; - const user = await storage.createUser(userData); + const user = await storage.createUserWithPassword(userData); res.json({ message: "User created successfully", @@ -623,6 +621,54 @@ export async function registerRoutes(app: Express): Promise { } }); + // 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 { diff --git a/server/storage.ts b/server/storage.ts index 9a34808..7d4a4f6 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -51,6 +51,11 @@ export interface IStorage { reactivateUser(id: string): Promise; makeAdmin(id: string): Promise; removeAdmin(id: string): Promise; + setUserPassword(id: string, password: string, isTemporary?: boolean): Promise; + deactivateUserSoft(id: string): Promise; + deleteUserHard(id: string): Promise; + getUserActiveStatus(id: string): Promise; + createUserWithPassword(userData: InsertUser & { password: string, isTemporary?: boolean }): Promise; // Event operations createEvent(event: InsertEvent): Promise; @@ -149,6 +154,18 @@ export class DatabaseStorage implements IStorage { return user; } + async createUserWithPassword(userData: InsertUser & { password: string, isTemporary?: boolean }): Promise { + const { password, isTemporary, ...userFields } = userData; + const [user] = await db.insert(users).values({ + ...userFields, + password, + tempPassword: isTemporary ? password : null, + needsPasswordChange: isTemporary || false, + isActive: true // Activate immediately when password is provided + }).returning(); + return user; + } + async upsertUser(userData: UpsertUser): Promise { const [user] = await db .insert(users) @@ -193,6 +210,55 @@ export class DatabaseStorage implements IStorage { await db.update(users).set({ role: "dj", updatedAt: new Date() }).where(eq(users.id, id)); } + async setUserPassword(id: string, password: string, isTemporary: boolean = false): Promise { + await db.update(users).set({ + password, + tempPassword: isTemporary ? password : null, + needsPasswordChange: isTemporary, + isActive: true, // Activate user when password is set + updatedAt: new Date() + }).where(eq(users.id, id)); + } + + async deactivateUserSoft(id: string): Promise { + // 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)); + } + + async deleteUserHard(id: string): Promise { + // Hard deletion - remove user and all future schedules/availability + const currentDate = new Date().toISOString().split('T')[0]; + + // Delete future availability + await db.delete(availability) + .where(and(eq(availability.djId, id), gte(availability.startDate, currentDate))); + + // Delete future events (keep past events for historical purposes) + await db.delete(events) + .where(and(eq(events.djId, id), gte(events.date, currentDate))); + + // Delete slot eligibility + await db.delete(slotEligibility).where(eq(slotEligibility.djId, id)); + + // Delete social links + await db.delete(socialLinks).where(eq(socialLinks.djId, id)); + + // Delete removal requests + await db.delete(removalRequests).where(eq(removalRequests.djId, id)); + + // Finally delete the user + await db.delete(users).where(eq(users.id, id)); + } + + async getUserActiveStatus(id: string): Promise { + const [user] = await db.select({ isActive: users.isActive, password: users.password }) + .from(users) + .where(eq(users.id, id)); + + // User is considered active if they have isActive=true AND have a password set + return user ? user.isActive && !!user.password : false; + } + // Event operations async createEvent(event: InsertEvent): Promise { const [newEvent] = await db.insert(events).values(event).returning(); diff --git a/shared/schema.ts b/shared/schema.ts index bb01042..aae94d7 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -43,6 +43,7 @@ export const users = pgTable("users", { role: userRoleEnum("role").default("dj").notNull(), isActive: boolean("is_active").default(true).notNull(), maxEventsPerMonth: integer("max_events_per_month").default(2).notNull(), + password: varchar("password"), tempPassword: varchar("temp_password"), needsPasswordChange: boolean("needs_password_change").default(false), createdAt: timestamp("created_at").defaultNow(),