Implement user registration, login, and logout endpoints with session management. Integrate Multer for video file uploads and extend the schema for user, video upload, category, and tag management. Replit-Commit-Author: Agent Replit-Commit-Session-Id: d7424866-83d1-4486-a212-ac12b4c7becf Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/d7424866-83d1-4486-a212-ac12b4c7becf/WCZ9oGO
416 lines
13 KiB
TypeScript
416 lines
13 KiB
TypeScript
import type { Express, Request, Response } from "express";
|
|
import { createServer, type Server } from "http";
|
|
import express from "express";
|
|
import { storage } from "./storage";
|
|
import { z } from "zod";
|
|
import {
|
|
updateVideoSchema, insertVideoSchema, insertUserSchema,
|
|
insertVideoUploadSchema, insertCategorySchema, insertTagSchema,
|
|
type User, type VideoUpload
|
|
} from "@shared/schema";
|
|
import multer from "multer";
|
|
import { randomUUID } from "crypto";
|
|
import path from "path";
|
|
import session from "express-session";
|
|
|
|
// Extend express session
|
|
declare module "express-session" {
|
|
interface SessionData {
|
|
userId: string;
|
|
}
|
|
}
|
|
|
|
// Configure multer for video uploads
|
|
const upload = multer({
|
|
storage: multer.diskStorage({
|
|
destination: './uploads/videos',
|
|
filename: (req, file, cb) => {
|
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
|
|
}
|
|
}),
|
|
limits: {
|
|
fileSize: 500 * 1024 * 1024, // 500MB max file size
|
|
},
|
|
fileFilter: (req, file, cb) => {
|
|
// Allow video files only
|
|
if (file.mimetype.startsWith('video/')) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Only video files are allowed'));
|
|
}
|
|
}
|
|
});
|
|
|
|
// Simple session-based authentication middleware
|
|
const authenticate = (req: Request, res: Response, next: any) => {
|
|
if (req.session?.userId) {
|
|
next();
|
|
} else {
|
|
res.status(401).json({ message: "Authentication required" });
|
|
}
|
|
};
|
|
|
|
export async function registerRoutes(app: Express): Promise<Server> {
|
|
// Configure session middleware
|
|
app.use(session({
|
|
secret: process.env.SESSION_SECRET || 'dev-secret-key',
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 } // 24 hours
|
|
}));
|
|
|
|
// Authentication Routes
|
|
app.post("/api/auth/register", async (req, res) => {
|
|
try {
|
|
const userData = insertUserSchema.parse(req.body);
|
|
|
|
// Check if user already exists
|
|
const existingUser = await storage.getUserByEmail(userData.email);
|
|
if (existingUser) {
|
|
return res.status(400).json({ message: "User already exists with this email" });
|
|
}
|
|
|
|
const user = await storage.createUser(userData);
|
|
|
|
// Remove password from response
|
|
const { password, ...userResponse } = user;
|
|
res.status(201).json(userResponse);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ message: "Invalid user data", errors: error.errors });
|
|
}
|
|
res.status(500).json({ message: "Failed to create user" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/auth/login", async (req, res) => {
|
|
try {
|
|
const { email, password } = req.body;
|
|
|
|
if (!email || !password) {
|
|
return res.status(400).json({ message: "Email and password are required" });
|
|
}
|
|
|
|
const user = await storage.validateUserPassword(email, password);
|
|
if (!user) {
|
|
return res.status(401).json({ message: "Invalid credentials" });
|
|
}
|
|
|
|
// Set session
|
|
req.session.userId = user.id;
|
|
|
|
// Remove password from response
|
|
const { password: _, ...userResponse } = user;
|
|
res.json(userResponse);
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to login" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/auth/logout", (req, res) => {
|
|
req.session?.destroy(() => {
|
|
res.json({ message: "Logged out successfully" });
|
|
});
|
|
});
|
|
|
|
app.get("/api/auth/me", authenticate, async (req, res) => {
|
|
try {
|
|
const user = await storage.getUser(req.session.userId!);
|
|
if (!user) {
|
|
return res.status(404).json({ message: "User not found" });
|
|
}
|
|
|
|
const { password, ...userResponse } = user;
|
|
res.json(userResponse);
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to fetch user" });
|
|
}
|
|
});
|
|
|
|
// Video Routes
|
|
app.get("/api/videos", async (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit as string) || 20;
|
|
const offset = parseInt(req.query.offset as string) || 0;
|
|
const search = req.query.search as string;
|
|
|
|
const videos = await storage.getVideos(limit, offset, search);
|
|
const total = await storage.getVideoCount(search);
|
|
|
|
res.json({
|
|
videos,
|
|
total,
|
|
hasMore: offset + limit < total
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to fetch videos" });
|
|
}
|
|
});
|
|
|
|
// Get single video by ID
|
|
app.get("/api/videos/:id", async (req, res) => {
|
|
try {
|
|
const video = await storage.getVideo(req.params.id);
|
|
if (!video) {
|
|
return res.status(404).json({ message: "Video not found" });
|
|
}
|
|
res.json(video);
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to fetch video" });
|
|
}
|
|
});
|
|
|
|
// Update video views
|
|
app.post("/api/videos/:id/view", async (req, res) => {
|
|
try {
|
|
await storage.updateVideoViews(req.params.id);
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to update views" });
|
|
}
|
|
});
|
|
|
|
// Create new video
|
|
app.post("/api/videos", authenticate, async (req, res) => {
|
|
try {
|
|
const videoData = insertVideoSchema.parse(req.body);
|
|
const video = await storage.createVideo(videoData);
|
|
res.status(201).json(video);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ message: "Invalid video data", errors: error.errors });
|
|
}
|
|
res.status(500).json({ message: "Failed to create video" });
|
|
}
|
|
});
|
|
|
|
// Update video metadata (title, description, etc.)
|
|
app.patch("/api/videos/:id", authenticate, async (req, res) => {
|
|
try {
|
|
const updates = updateVideoSchema.parse(req.body);
|
|
const video = await storage.updateVideo(req.params.id, updates);
|
|
|
|
if (!video) {
|
|
return res.status(404).json({ message: "Video not found" });
|
|
}
|
|
|
|
res.json(video);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ message: "Invalid request data", errors: error.errors });
|
|
}
|
|
res.status(500).json({ message: "Failed to update video" });
|
|
}
|
|
});
|
|
|
|
// Delete video
|
|
app.delete("/api/videos/:id", authenticate, async (req, res) => {
|
|
try {
|
|
const success = await storage.deleteVideo(req.params.id);
|
|
if (!success) {
|
|
return res.status(404).json({ message: "Video not found" });
|
|
}
|
|
res.json({ success: true, message: "Video deleted successfully" });
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to delete video" });
|
|
}
|
|
});
|
|
|
|
// Video Upload Routes
|
|
app.post("/api/uploads/start", authenticate, async (req, res) => {
|
|
try {
|
|
const { originalFileName, fileSize, mimeType } = req.body;
|
|
|
|
if (!originalFileName || !fileSize || !mimeType) {
|
|
return res.status(400).json({
|
|
message: "originalFileName, fileSize, and mimeType are required"
|
|
});
|
|
}
|
|
|
|
const uploadData = {
|
|
userId: req.session.userId!,
|
|
originalFileName,
|
|
fileSize: parseInt(fileSize),
|
|
mimeType,
|
|
uploadStatus: "uploading" as const,
|
|
uploadProgress: 0
|
|
};
|
|
|
|
const upload = await storage.createVideoUpload(uploadData);
|
|
res.status(201).json(upload);
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to initialize upload" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/uploads/:id/video", authenticate, upload.single('video'), async (req, res) => {
|
|
try {
|
|
const uploadId = req.params.id;
|
|
const file = req.file;
|
|
|
|
if (!file) {
|
|
return res.status(400).json({ message: "No video file provided" });
|
|
}
|
|
|
|
// Update upload with file information
|
|
await storage.updateVideoUpload(uploadId, {
|
|
uploadStatus: "processing",
|
|
uploadProgress: 1.0
|
|
});
|
|
|
|
// Create video record
|
|
const videoData = {
|
|
title: req.body.title || path.parse(file.originalname).name,
|
|
description: req.body.description || "",
|
|
thumbnailUrl: req.body.thumbnailUrl || "https://via.placeholder.com/800x450",
|
|
videoUrl: `/uploads/videos/${file.filename}`,
|
|
duration: parseInt(req.body.duration) || 0,
|
|
views: 0,
|
|
category: req.body.category || "",
|
|
tags: req.body.tags ? JSON.parse(req.body.tags) : [],
|
|
isPublic: req.body.isPublic !== "false",
|
|
uploadStatus: "completed",
|
|
originalFileName: file.originalname,
|
|
fileSize: file.size,
|
|
format: path.extname(file.originalname).slice(1)
|
|
};
|
|
|
|
const video = await storage.createVideo(videoData);
|
|
|
|
// Link video to upload
|
|
await storage.updateVideoUpload(uploadId, {
|
|
videoId: video.id,
|
|
uploadStatus: "completed"
|
|
});
|
|
|
|
res.json({ video, upload: { id: uploadId, status: "completed" } });
|
|
} catch (error) {
|
|
console.error("Upload error:", error);
|
|
|
|
// Update upload status to failed
|
|
if (req.params.id) {
|
|
await storage.updateVideoUpload(req.params.id, {
|
|
uploadStatus: "failed",
|
|
errorMessage: error instanceof Error ? error.message : "Upload failed"
|
|
});
|
|
}
|
|
|
|
res.status(500).json({ message: "Failed to upload video" });
|
|
}
|
|
});
|
|
|
|
app.get("/api/uploads/:id/status", authenticate, async (req, res) => {
|
|
try {
|
|
const upload = await storage.getVideoUpload(req.params.id);
|
|
if (!upload) {
|
|
return res.status(404).json({ message: "Upload not found" });
|
|
}
|
|
res.json(upload);
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to get upload status" });
|
|
}
|
|
});
|
|
|
|
app.get("/api/uploads/user", authenticate, async (req, res) => {
|
|
try {
|
|
const uploads = await storage.getUserVideoUploads(req.session.userId!);
|
|
res.json(uploads);
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to fetch user uploads" });
|
|
}
|
|
});
|
|
|
|
// Category Routes
|
|
app.get("/api/categories", async (req, res) => {
|
|
try {
|
|
const categories = await storage.getCategories();
|
|
res.json(categories);
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to fetch categories" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/categories", authenticate, async (req, res) => {
|
|
try {
|
|
const categoryData = insertCategorySchema.parse(req.body);
|
|
const category = await storage.createCategory(categoryData);
|
|
res.status(201).json(category);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ message: "Invalid category data", errors: error.errors });
|
|
}
|
|
res.status(500).json({ message: "Failed to create category" });
|
|
}
|
|
});
|
|
|
|
app.patch("/api/categories/:id", authenticate, async (req, res) => {
|
|
try {
|
|
const updates = insertCategorySchema.partial().parse(req.body);
|
|
const category = await storage.updateCategory(req.params.id, updates);
|
|
|
|
if (!category) {
|
|
return res.status(404).json({ message: "Category not found" });
|
|
}
|
|
|
|
res.json(category);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ message: "Invalid category data", errors: error.errors });
|
|
}
|
|
res.status(500).json({ message: "Failed to update category" });
|
|
}
|
|
});
|
|
|
|
app.delete("/api/categories/:id", authenticate, async (req, res) => {
|
|
try {
|
|
const success = await storage.deleteCategory(req.params.id);
|
|
if (!success) {
|
|
return res.status(404).json({ message: "Category not found" });
|
|
}
|
|
res.json({ success: true, message: "Category deleted successfully" });
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to delete category" });
|
|
}
|
|
});
|
|
|
|
// Tag Routes
|
|
app.get("/api/tags", async (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit as string) || 50;
|
|
const popular = req.query.popular === "true";
|
|
|
|
const tags = popular
|
|
? await storage.getPopularTags(limit)
|
|
: await storage.getTags();
|
|
|
|
res.json(tags);
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Failed to fetch tags" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/tags", authenticate, async (req, res) => {
|
|
try {
|
|
const tagData = insertTagSchema.parse(req.body);
|
|
const tag = await storage.createTag(tagData);
|
|
res.status(201).json(tag);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ message: "Invalid tag data", errors: error.errors });
|
|
}
|
|
res.status(500).json({ message: "Failed to create tag" });
|
|
}
|
|
});
|
|
|
|
// Serve uploaded videos
|
|
app.use('/uploads', express.static('uploads'));
|
|
|
|
|
|
|
|
|
|
|
|
const httpServer = createServer(app);
|
|
return httpServer;
|
|
}
|