Add user authentication and video upload capabilities
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
This commit is contained in:
+343
-22
@@ -1,11 +1,134 @@
|
||||
import type { Express } from "express";
|
||||
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 } from "@shared/schema";
|
||||
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> {
|
||||
// Get videos with pagination and filtering
|
||||
// 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;
|
||||
@@ -48,43 +171,241 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// 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", async (req, res) => {
|
||||
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);
|
||||
|
||||
// For Bunny.net integration, metadata updates are not supported via API
|
||||
// This endpoint acknowledges the request but doesn't actually update Bunny.net
|
||||
console.log(`Metadata update requested for video ${req.params.id}:`, updates);
|
||||
if (!video) {
|
||||
return res.status(404).json({ message: "Video not found" });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Metadata saved locally (Bunny.net integration limits remote updates)"
|
||||
});
|
||||
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 process update request" });
|
||||
res.status(500).json({ message: "Failed to update video" });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete video
|
||||
app.delete("/api/videos/:id", async (req, res) => {
|
||||
app.delete("/api/videos/:id", authenticate, async (req, res) => {
|
||||
try {
|
||||
// For Bunny.net integration, video deletion is not supported via API
|
||||
// This endpoint acknowledges the request but doesn't actually delete from Bunny.net
|
||||
console.log(`Video deletion requested for video ${req.params.id}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Deletion requested (Bunny.net integration limits remote deletion)"
|
||||
});
|
||||
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 process deletion request" });
|
||||
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'));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user