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:
+104
-26
@@ -38,15 +38,40 @@ import {
|
||||
} from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
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 {
|
||||
// User operations (IMPORTANT: mandatory for Replit Auth)
|
||||
getUser(id: string): Promise<User | undefined>;
|
||||
getUserByEmail(email: string): Promise<User | undefined>;
|
||||
upsertUser(user: UpsertUser): Promise<User>;
|
||||
createUser(user: InsertUser): Promise<User>;
|
||||
updateUser(id: string, updates: UpdateUser): Promise<User>;
|
||||
getAllDJs(): Promise<User[]>;
|
||||
getUser(id: string): Promise<SafeUser | undefined>;
|
||||
getUserByEmail(email: string): Promise<SafeUser | undefined>;
|
||||
upsertUser(user: UpsertUser): Promise<SafeUser>;
|
||||
createUser(user: InsertUser): Promise<SafeUser>;
|
||||
updateUser(id: string, updates: UpdateUser): Promise<SafeUser>;
|
||||
getAllDJs(): Promise<(SafeUser & { derivedActive: boolean })[]>;
|
||||
deactivateUser(id: string): Promise<void>;
|
||||
reactivateUser(id: string): Promise<void>;
|
||||
makeAdmin(id: string): Promise<void>;
|
||||
@@ -55,7 +80,9 @@ export interface IStorage {
|
||||
deactivateUserSoft(id: string): Promise<void>;
|
||||
deleteUserHard(id: string): Promise<void>;
|
||||
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
|
||||
createEvent(event: InsertEvent): Promise<Event>;
|
||||
@@ -139,34 +166,35 @@ export interface IStorage {
|
||||
|
||||
export class DatabaseStorage implements IStorage {
|
||||
// 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));
|
||||
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));
|
||||
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();
|
||||
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 hashedPassword = await hashPassword(password);
|
||||
const [user] = await db.insert(users).values({
|
||||
...userFields,
|
||||
password,
|
||||
tempPassword: isTemporary ? password : null,
|
||||
password: hashedPassword,
|
||||
tempPassword: isTemporary ? hashedPassword : null,
|
||||
needsPasswordChange: isTemporary || false,
|
||||
isActive: true // Activate immediately when password is provided
|
||||
}).returning();
|
||||
return user;
|
||||
return sanitizeUser(user);
|
||||
}
|
||||
|
||||
async upsertUser(userData: UpsertUser): Promise<User> {
|
||||
async upsertUser(userData: UpsertUser): Promise<SafeUser> {
|
||||
const [user] = await db
|
||||
.insert(users)
|
||||
.values(userData)
|
||||
@@ -178,20 +206,29 @@ export class DatabaseStorage implements IStorage {
|
||||
},
|
||||
})
|
||||
.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
|
||||
.update(users)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(eq(users.id, id))
|
||||
.returning();
|
||||
return user;
|
||||
return sanitizeUser(user);
|
||||
}
|
||||
|
||||
async getAllDJs(): Promise<User[]> {
|
||||
return await db.select().from(users).where(eq(users.role, "dj")).orderBy(asc(users.displayName));
|
||||
async getAllDJs(): Promise<(SafeUser & { derivedActive: boolean })[]> {
|
||||
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> {
|
||||
@@ -211,9 +248,10 @@ export class DatabaseStorage implements IStorage {
|
||||
}
|
||||
|
||||
async setUserPassword(id: string, password: string, isTemporary: boolean = false): Promise<void> {
|
||||
const hashedPassword = await hashPassword(password);
|
||||
await db.update(users).set({
|
||||
password,
|
||||
tempPassword: isTemporary ? password : null,
|
||||
password: hashedPassword,
|
||||
tempPassword: isTemporary ? hashedPassword : null,
|
||||
needsPasswordChange: isTemporary,
|
||||
isActive: true, // Activate user when password is set
|
||||
updatedAt: new Date()
|
||||
@@ -222,7 +260,14 @@ export class DatabaseStorage implements IStorage {
|
||||
|
||||
async deactivateUserSoft(id: string): Promise<void> {
|
||||
// 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> {
|
||||
@@ -259,6 +304,39 @@ export class DatabaseStorage implements IStorage {
|
||||
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
|
||||
async createEvent(event: InsertEvent): Promise<Event> {
|
||||
const [newEvent] = await db.insert(events).values(event).returning();
|
||||
|
||||
Reference in New Issue
Block a user