Add admin capabilities for managing users on the platform

Introduce new API endpoints and backend logic for administrative user management, including fetching, viewing, and updating user roles, while ensuring sensitive data like password hashes are excluded from responses. Implement Replit user ID formatting for internal consistency and extend storage interfaces to support user retrieval and counting.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 170e18f0-0f13-4eca-8643-546bba1dd8cc
Replit-Commit-Checkpoint-Type: full_checkpoint
This commit is contained in:
sebastjanartic 2025-09-02 12:19:53 +00:00
parent d283a0d4a5
commit 68bb99739b
3 changed files with 214 additions and 11 deletions

View File

@ -54,8 +54,11 @@ function updateUserSession(
}
async function upsertUser(claims: any) {
// Convert Replit numeric ID to string format compatible with our schema
const userId = `replit_${claims["sub"]}`;
await storage.upsertUser({
id: claims["sub"],
id: userId,
email: claims["email"],
firstName: claims["first_name"],
lastName: claims["last_name"],
@ -160,7 +163,9 @@ export const isAdmin: RequestHandler = async (req, res, next) => {
}
try {
const dbUser = await storage.getUser(user.claims.sub);
// Convert Replit numeric ID to our internal format
const userId = `replit_${user.claims.sub}`;
const dbUser = await storage.getUser(userId);
if (!dbUser || !dbUser.isAdmin) {
return res.status(403).json({ message: "Admin access required" });
}

View File

@ -96,7 +96,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
const user = await storage.createUser(userData);
// Remove password from response
const { password, ...userResponse } = user;
const { passwordHash, ...userResponse } = user;
res.status(201).json(userResponse);
} catch (error) {
if (error instanceof z.ZodError) {
@ -123,7 +123,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
req.session.userId = user.id;
// Remove password from response
const { password: _, ...userResponse } = user;
const { passwordHash, ...userResponse } = user;
res.json(userResponse);
} catch (error) {
res.status(500).json({ message: "Failed to login" });
@ -143,7 +143,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
return res.status(404).json({ message: "User not found" });
}
const { password, ...userResponse } = user;
const { passwordHash, ...userResponse } = user;
res.json(userResponse);
} catch (error) {
res.status(500).json({ message: "Failed to fetch user" });
@ -860,7 +860,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
// Auth route to get current user
app.get('/api/auth/user', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims.sub;
const userId = `replit_${req.user.claims.sub}`;
const user = await storage.getUser(userId);
res.json(user);
} catch (error) {
@ -934,10 +934,54 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// User management (super admin only)
// ===== USER MANAGEMENT ROUTES (ADMIN) =====
// Get all users (admin only)
app.get('/api/admin/users', isAdmin, async (req, res) => {
try {
const limit = parseInt(req.query.limit as string) || 50;
const offset = parseInt(req.query.offset as string) || 0;
const search = req.query.search as string;
// For now, get all users - in production you'd want pagination
const users = await storage.getAllUsers ? await storage.getAllUsers(limit, offset, search) : [];
// Remove password hashes from response
const safeUsers = users.map(({ passwordHash, ...user }) => user);
res.json({
users: safeUsers,
total: users.length,
limit,
offset
});
} catch (error) {
console.error("Error fetching users:", error);
res.status(500).json({ message: "Failed to fetch users" });
}
});
// Get user by ID (admin only)
app.get('/api/admin/users/:id', isAdmin, async (req, res) => {
try {
const user = await storage.getUser(req.params.id);
if (!user) {
return res.status(404).json({ message: "User not found" });
}
// Remove password hash from response
const { passwordHash, ...safeUser } = user;
res.json(safeUser);
} catch (error) {
console.error("Error fetching user:", error);
res.status(500).json({ message: "Failed to fetch user" });
}
});
// Update user admin status (super admin only)
app.patch('/api/admin/users/:id/admin', isAuthenticated, async (req: any, res) => {
try {
const currentUserId = req.user.claims.sub;
const currentUserId = `replit_${req.user.claims.sub}`;
const currentUser = await storage.getUser(currentUserId);
if (!currentUser?.isSuperAdmin) {
@ -952,13 +996,71 @@ export async function registerRoutes(app: Express): Promise<Server> {
return res.status(404).json({ message: "User not found" });
}
res.json(updatedUser);
// Remove password hash from response
const { passwordHash, ...safeUser } = updatedUser;
res.json(safeUser);
} catch (error) {
console.error("Error updating user admin status:", error);
res.status(500).json({ message: "Failed to update user" });
}
});
// Update user active status (admin only)
app.patch('/api/admin/users/:id/status', isAdmin, async (req, res) => {
try {
const userId = req.params.id;
const { isActive } = req.body;
if (typeof isActive !== 'boolean') {
return res.status(400).json({ message: "isActive must be a boolean" });
}
const updatedUser = await storage.updateUser(userId, { isActive });
if (!updatedUser) {
return res.status(404).json({ message: "User not found" });
}
// Remove password hash from response
const { passwordHash, ...safeUser } = updatedUser;
res.json(safeUser);
} catch (error) {
console.error("Error updating user status:", error);
res.status(500).json({ message: "Failed to update user status" });
}
});
// Get admin statistics (admin only)
app.get('/api/admin/stats', isAdmin, async (req, res) => {
try {
const videoCount = await storage.getVideoCount();
const userCount = await storage.getUserCount ? await storage.getUserCount() : 0;
// Get recent videos
const recentVideos = await storage.getVideos(5, 0);
// Basic stats - in production you might want more sophisticated analytics
const stats = {
videos: {
total: videoCount,
recent: recentVideos.length
},
users: {
total: userCount,
// Could add more user analytics here
},
platform: {
uptime: process.uptime(),
nodeVersion: process.version
}
};
res.json(stats);
} catch (error) {
console.error("Error fetching admin stats:", error);
res.status(500).json({ message: "Failed to fetch statistics" });
}
});
const httpServer = createServer(app);
return httpServer;
}

View File

@ -27,6 +27,8 @@ export interface IStorage {
getUser(id: string): Promise<User | undefined>;
getUserByEmail(email: string): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
getAllUsers(limit?: number, offset?: number, search?: string): Promise<User[]>;
getUserCount(search?: string): Promise<number>;
createUser(user: InsertUser): Promise<User>;
updateUser(id: string, user: Partial<InsertUser>): Promise<User | undefined>;
upsertUser(user: any): Promise<User>;
@ -144,11 +146,12 @@ export class DatabaseStorage implements IStorage {
// Hash password before storing
const hashedPassword = await bcrypt.hash(user.passwordHash, 12);
const { passwordHash: _, ...userWithoutPassword } = user;
const result = await db.insert(users).values({
...user,
...userWithoutPassword,
passwordHash: hashedPassword,
updatedAt: new Date()
}).returning();
} as any).returning();
return result[0];
}
@ -195,6 +198,48 @@ export class DatabaseStorage implements IStorage {
return user;
}
async getAllUsers(limit = 50, offset = 0, search?: string): Promise<User[]> {
let query = db.select().from(users);
if (search) {
const searchTerm = `%${search}%`;
query = query.where(
or(
like(users.username, searchTerm),
like(users.email, searchTerm),
like(users.firstName, searchTerm),
like(users.lastName, searchTerm)
)
) as any;
}
const result = await query
.orderBy(desc(users.createdAt))
.limit(limit)
.offset(offset);
return result;
}
async getUserCount(search?: string): Promise<number> {
let query = db.select({ count: sql<number>`count(*)` }).from(users);
if (search) {
const searchTerm = `%${search}%`;
query = query.where(
or(
like(users.username, searchTerm),
like(users.email, searchTerm),
like(users.firstName, searchTerm),
like(users.lastName, searchTerm)
)
) as any;
}
const result = await query;
return result[0].count;
}
async validateUserPassword(email: string, password: string): Promise<User | null> {
const user = await this.getUserByEmail(email);
if (!user) return null;
@ -582,6 +627,41 @@ export class MemStorage implements IStorage {
return user;
}
async getAllUsers(limit = 50, offset = 0, search?: string): Promise<User[]> {
let users = Array.from(this.users.values());
if (search && search.length >= 2) {
const searchLower = search.toLowerCase();
users = users.filter(user =>
user.username.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower) ||
user.firstName?.toLowerCase().includes(searchLower) ||
user.lastName?.toLowerCase().includes(searchLower)
);
}
// Sort by created date (newest first)
users.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return users.slice(offset, offset + limit);
}
async getUserCount(search?: string): Promise<number> {
let users = Array.from(this.users.values());
if (search && search.length >= 2) {
const searchLower = search.toLowerCase();
users = users.filter(user =>
user.username.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower) ||
user.firstName?.toLowerCase().includes(searchLower) ||
user.lastName?.toLowerCase().includes(searchLower)
);
}
return users.length;
}
async validateUserPassword(email: string, password: string): Promise<User | null> {
const user = await this.getUserByEmail(email);
if (!user) return null;
@ -824,6 +904,14 @@ class BunnyStorage implements IStorage {
throw new Error("User operations are not supported with Bunny.net integration");
}
async getAllUsers(limit?: number, offset?: number, search?: string): Promise<User[]> {
throw new Error("User operations are not supported with Bunny.net integration");
}
async getUserCount(search?: string): Promise<number> {
throw new Error("User operations are not supported with Bunny.net integration");
}
async validateUserPassword(email: string, password: string): Promise<User | null> {
throw new Error("User operations are not supported with Bunny.net integration");
}
@ -976,6 +1064,14 @@ export class HybridStorage implements IStorage {
return this.databaseStorage.upsertUser(user);
}
async getAllUsers(limit?: number, offset?: number, search?: string): Promise<User[]> {
return this.databaseStorage.getAllUsers(limit, offset, search);
}
async getUserCount(search?: string): Promise<number> {
return this.databaseStorage.getUserCount(search);
}
async validateUserPassword(email: string, password: string): Promise<User | null> {
return this.databaseStorage.validateUserPassword(email, password);
}