diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..bcdba3d --- /dev/null +++ b/cookies.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_localhost FALSE / FALSE 1754764079 connect.sid s%3AoF8eUXaMkrpaD21vFyWEEVekqgVtVD-z.mFy23jRV5iiDuxeEQGbmFtMWcfu6QjCmMOAV0fWT%2BJo diff --git a/package-lock.json b/package-lock.json index db044b7..99c54e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,9 @@ "@radix-ui/react-toggle-group": "^1.1.3", "@radix-ui/react-tooltip": "^1.2.0", "@tanstack/react-query": "^5.60.5", + "@types/bcryptjs": "^2.4.6", "@types/memoizee": "^0.4.12", + "@types/multer": "^2.0.0", "@types/video.js": "^7.3.58", "@uppy/aws-s3": "^4.3.2", "@uppy/core": "^4.5.2", @@ -51,6 +53,7 @@ "@uppy/file-input": "^4.2.2", "@uppy/progress-bar": "^4.3.2", "@uppy/react": "^4.5.2", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -60,7 +63,7 @@ "drizzle-zod": "^0.7.0", "embla-carousel-react": "^8.6.0", "express": "^4.21.2", - "express-session": "^1.18.1", + "express-session": "^1.18.2", "framer-motion": "^11.13.1", "google-auth-library": "^10.2.1", "hls.js": "^1.6.7", @@ -68,6 +71,7 @@ "lucide-react": "^0.453.0", "memoizee": "^0.4.17", "memorystore": "^1.6.7", + "multer": "^2.0.2", "next-themes": "^0.4.6", "openid-client": "^6.6.3", "passport": "^0.7.0", @@ -99,7 +103,7 @@ "@tailwindcss/vite": "^4.1.3", "@types/connect-pg-simple": "^7.0.3", "@types/express": "4.17.21", - "@types/express-session": "^1.18.0", + "@types/express-session": "^1.18.2", "@types/node": "20.16.11", "@types/passport": "^1.0.16", "@types/passport-local": "^1.0.38", @@ -3840,11 +3844,16 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -3861,7 +3870,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -3953,7 +3961,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -3966,7 +3973,6 @@ "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -3976,9 +3982,9 @@ } }, "node_modules/@types/express-session": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.0.tgz", - "integrity": "sha512-27JdDRgor6PoYlURY+Y5kCakqp5ulC0kmf7y+QwaY+hv9jEFuQOThgkjyA53RP3jmKuBsH5GR6qEfFmvb8mwOA==", + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", "dev": true, "license": "MIT", "dependencies": { @@ -3989,7 +3995,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, "license": "MIT" }, "node_modules/@types/memoizee": { @@ -4002,9 +4007,17 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.16.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", @@ -4069,14 +4082,12 @@ "version": "6.9.16", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { @@ -4122,7 +4133,6 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -4133,7 +4143,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -4853,6 +4862,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4965,6 +4980,15 @@ ], "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -5089,7 +5113,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/bufferutil": { @@ -5106,6 +5129,17 @@ "node": ">=6.14.2" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5385,6 +5419,21 @@ "node": ">= 6" } }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/connect-pg-simple": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz", @@ -6712,16 +6761,16 @@ } }, "node_modules/express-session": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", - "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", "license": "MIT", "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.7", "debug": "2.6.9", "depd": "~2.0.0", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "parseurl": "~1.3.3", "safe-buffer": "5.2.1", "uid-safe": "~2.1.5" @@ -8204,6 +8253,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -8219,6 +8277,18 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/modern-screenshot": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/modern-screenshot/-/modern-screenshot-4.6.0.tgz", @@ -8256,6 +8326,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/mux.js": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz", @@ -8465,9 +8553,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -9877,6 +9965,14 @@ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "license": "MIT" }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -10710,6 +10806,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", diff --git a/package.json b/package.json index 2b56544..fa6e924 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,9 @@ "@radix-ui/react-toggle-group": "^1.1.3", "@radix-ui/react-tooltip": "^1.2.0", "@tanstack/react-query": "^5.60.5", + "@types/bcryptjs": "^2.4.6", "@types/memoizee": "^0.4.12", + "@types/multer": "^2.0.0", "@types/video.js": "^7.3.58", "@uppy/aws-s3": "^4.3.2", "@uppy/core": "^4.5.2", @@ -53,6 +55,7 @@ "@uppy/file-input": "^4.2.2", "@uppy/progress-bar": "^4.3.2", "@uppy/react": "^4.5.2", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -62,7 +65,7 @@ "drizzle-zod": "^0.7.0", "embla-carousel-react": "^8.6.0", "express": "^4.21.2", - "express-session": "^1.18.1", + "express-session": "^1.18.2", "framer-motion": "^11.13.1", "google-auth-library": "^10.2.1", "hls.js": "^1.6.7", @@ -70,6 +73,7 @@ "lucide-react": "^0.453.0", "memoizee": "^0.4.17", "memorystore": "^1.6.7", + "multer": "^2.0.2", "next-themes": "^0.4.6", "openid-client": "^6.6.3", "passport": "^0.7.0", @@ -101,7 +105,7 @@ "@tailwindcss/vite": "^4.1.3", "@types/connect-pg-simple": "^7.0.3", "@types/express": "4.17.21", - "@types/express-session": "^1.18.0", + "@types/express-session": "^1.18.2", "@types/node": "20.16.11", "@types/passport": "^1.0.16", "@types/passport-local": "^1.0.38", diff --git a/replit.md b/replit.md index fe9ea57..e22e93b 100644 --- a/replit.md +++ b/replit.md @@ -2,10 +2,16 @@ ## Overview -go4.video is a fully functional professional video streaming platform that integrates directly with Bunny.net CDN for secure video delivery. The application successfully streams 85 private videos from the user's Bunny.net library through a custom web interface. It features a YouTube-like design with video cards, search functionality, and a professional video player with comprehensive controls including central play button, progress bar, volume control, time display, and fullscreen capabilities. +go4.video is a fully functional professional video streaming platform with a comprehensive backend infrastructure for video upload, editing, and management. The application features both Bunny.net CDN integration for existing content and a complete PostgreSQL-based system for new video uploads and user management. It includes a YouTube-like design with video cards, search functionality, and a professional video player with comprehensive controls including central play button, progress bar, volume control, time display, and fullscreen capabilities. ## Recent Changes (August 2025) +- ✅ **Complete Backend Infrastructure**: Full PostgreSQL database with user authentication, video upload tracking, categories, and tags management +- ✅ **Video Upload System**: Comprehensive video upload functionality with progress tracking, metadata editing, and file management +- ✅ **User Authentication**: Session-based authentication system with registration, login, and user management +- ✅ **Database Schema**: Complete database schema with users, videos, video_uploads, categories, tags, and video_tags tables +- ✅ **Storage Abstraction**: Flexible storage system supporting PostgreSQL database, Bunny.net integration, and memory storage with automatic fallback +- ✅ **REST API**: Comprehensive REST API with authentication, video CRUD operations, upload management, and category/tag systems - ✅ **HLS.js Video Streaming**: Reliable HLS.js implementation for professional video streaming with Bunny.net integration - ✅ **Advanced Video Controls**: Professional video player with fluid responsive design and adaptive streaming - ✅ **Video Controls**: Professional video player with full controls and responsive design @@ -42,31 +48,40 @@ The frontend is built using **React** with **TypeScript** and follows a modern c The frontend uses a **component composition pattern** with separate components for video cards, search headers, video grids, and modal players. This approach promotes reusability and maintains clean separation of concerns. ### Backend Architecture -The backend follows a **REST API pattern** using **Express.js**: +The backend follows a **comprehensive REST API pattern** using **Express.js** with full CRUD operations: - **Server Framework**: Express.js with TypeScript for type-safe server development -- **API Design**: RESTful endpoints for video operations (GET, POST) with pagination support -- **Storage Layer**: Abstracted storage interface allowing for multiple implementations (currently using in-memory storage with plans for database integration) -- **Middleware**: Custom logging middleware for API request tracking and error handling +- **API Design**: Complete RESTful API with authentication, video management, upload tracking, categories, and tags +- **Authentication**: Session-based authentication with bcrypt password hashing and user management +- **Storage Layer**: Flexible abstracted storage interface with PostgreSQL database, Bunny.net integration, and memory storage fallback +- **Upload System**: Comprehensive video upload system with progress tracking, file handling via Multer, and metadata management +- **Middleware**: Authentication middleware, session management, file upload handling, and error processing +- **Database Integration**: Full PostgreSQL integration with Drizzle ORM and comprehensive schema management -The server implements a **middleware pattern** for request processing and includes development-specific tooling like Vite integration for hot module replacement. +The server implements a **layered architecture pattern** with storage abstraction, authentication middleware, and comprehensive video management capabilities. ### Data Storage Architecture -The application uses a **flexible storage abstraction**: +The application uses a **comprehensive multi-tier storage system**: -- **ORM**: Drizzle ORM configured for PostgreSQL with type-safe database operations -- **Database**: PostgreSQL (configured via Neon Database) for production data persistence -- **Schema Management**: Centralized schema definitions in TypeScript with automatic type generation -- **Development Storage**: In-memory storage implementation for development and testing +- **Primary Storage**: PostgreSQL database with full schema including users, videos, video_uploads, categories, tags, and video_tags tables +- **ORM**: Drizzle ORM configured for PostgreSQL with complete type-safe database operations and relationship management +- **Database**: PostgreSQL (configured via environment variables) for production data persistence with automatic fallback +- **Schema Management**: Comprehensive schema definitions in TypeScript with automatic type generation and validation +- **Storage Abstraction**: Interface-based design supporting multiple storage backends (DatabaseStorage, BunnyStorage, MemStorage) +- **File Storage**: Local file system for video uploads with configurable upload directory and file management +- **CDN Integration**: Bunny.net CDN integration for existing video content with secure signed URL authentication -The storage layer uses an **interface-based design** allowing easy switching between storage implementations without affecting business logic. +The storage layer implements a **strategic fallback pattern**: PostgreSQL database first, Bunny.net integration second, memory storage as final fallback, ensuring reliability across different deployment scenarios. ### Authentication & Authorization -Currently uses **session-based architecture**: +Complete **session-based authentication system**: -- **Session Management**: Express sessions with PostgreSQL session store (connect-pg-simple) -- **Security**: CORS configuration and secure session handling -- **Future-Ready**: Architecture prepared for JWT or OAuth integration +- **User Management**: Full user registration, login, and profile management with bcrypt password hashing +- **Session Management**: Express sessions with configurable session store and secure cookie handling +- **Authentication Middleware**: Comprehensive authentication middleware protecting all sensitive routes +- **Security**: CORS configuration, secure session handling, and password encryption with bcrypt (12 rounds) +- **API Protection**: All video upload, editing, and management operations require authentication +- **User Data**: User profiles with username, email, admin privileges, and profile management capabilities ### Build & Development Architecture The application uses a **monorepo structure** with shared code: diff --git a/server/routes.ts b/server/routes.ts index 1f9acea..ecba611 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -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 { - // 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 { } }); + // 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')); + diff --git a/server/storage.ts b/server/storage.ts index a0c1499..c471c24 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -1,23 +1,297 @@ -import { type Video, type InsertVideo, type UpdateVideo } from "@shared/schema"; +import { + type Video, type InsertVideo, type UpdateVideo, + type User, type InsertUser, + type VideoUpload, type InsertVideoUpload, + type Category, type InsertCategory, + type Tag, type InsertTag, + videos, users, videoUploads, categories, tags, videoTags +} from "@shared/schema"; import { randomUUID } from "crypto"; import { BunnyService } from "./bunny"; +import { db } from "./db"; +import { eq, desc, asc, like, or, sql, and } from "drizzle-orm"; +import bcrypt from "bcryptjs"; export interface IStorage { + // Video operations getVideos(limit?: number, offset?: number, search?: string): Promise; getVideo(id: string): Promise