diff --git a/server/replitAuth.ts b/server/replitAuth.ts index 0065d76..8411e88 100644 --- a/server/replitAuth.ts +++ b/server/replitAuth.ts @@ -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" }); } diff --git a/server/routes.ts b/server/routes.ts index cf85cc4..aa78b69 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -96,7 +96,7 @@ export async function registerRoutes(app: Express): Promise { 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 { 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 { 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 { // 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 { } }); - // 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 { 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; } diff --git a/server/storage.ts b/server/storage.ts index c0f80c1..102fbb7 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -27,6 +27,8 @@ export interface IStorage { getUser(id: string): Promise; getUserByEmail(email: string): Promise; getUserByUsername(username: string): Promise; + getAllUsers(limit?: number, offset?: number, search?: string): Promise; + getUserCount(search?: string): Promise; createUser(user: InsertUser): Promise; updateUser(id: string, user: Partial): Promise; upsertUser(user: any): Promise; @@ -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 { + 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 { + let query = db.select({ count: sql`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 { 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 { + 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 { + 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 { 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 { + throw new Error("User operations are not supported with Bunny.net integration"); + } + + async getUserCount(search?: string): Promise { + throw new Error("User operations are not supported with Bunny.net integration"); + } + async validateUserPassword(email: string, password: string): Promise { 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 { + return this.databaseStorage.getAllUsers(limit, offset, search); + } + + async getUserCount(search?: string): Promise { + return this.databaseStorage.getUserCount(search); + } + async validateUserPassword(email: string, password: string): Promise { return this.databaseStorage.validateUserPassword(email, password); }