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:
parent
56a8c264dd
commit
257e16e8d2
5
cookies.txt
Normal file
5
cookies.txt
Normal file
@ -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
|
||||
148
package-lock.json
generated
148
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
47
replit.md
47
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:
|
||||
|
||||
365
server/routes.ts
365
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<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'));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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<Video[]>;
|
||||
getVideo(id: string): Promise<Video | undefined>;
|
||||
createVideo(video: InsertVideo): Promise<Video>;
|
||||
updateVideo(id: string, video: UpdateVideo): Promise<Video | undefined>;
|
||||
updateVideoViews(id: string): Promise<void>;
|
||||
getVideoCount(search?: string): Promise<number>;
|
||||
deleteVideo(id: string): Promise<boolean>;
|
||||
|
||||
// User operations
|
||||
getUser(id: string): Promise<User | undefined>;
|
||||
getUserByEmail(email: string): Promise<User | undefined>;
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
createUser(user: InsertUser): Promise<User>;
|
||||
updateUser(id: string, user: Partial<InsertUser>): Promise<User | undefined>;
|
||||
validateUserPassword(email: string, password: string): Promise<User | null>;
|
||||
|
||||
// Upload operations
|
||||
createVideoUpload(upload: InsertVideoUpload): Promise<VideoUpload>;
|
||||
getVideoUpload(id: string): Promise<VideoUpload | undefined>;
|
||||
updateVideoUpload(id: string, upload: Partial<InsertVideoUpload>): Promise<VideoUpload | undefined>;
|
||||
getUserVideoUploads(userId: string): Promise<VideoUpload[]>;
|
||||
|
||||
// Category operations
|
||||
getCategories(): Promise<Category[]>;
|
||||
createCategory(category: InsertCategory): Promise<Category>;
|
||||
updateCategory(id: string, category: Partial<InsertCategory>): Promise<Category | undefined>;
|
||||
deleteCategory(id: string): Promise<boolean>;
|
||||
|
||||
// Tag operations
|
||||
getTags(): Promise<Tag[]>;
|
||||
getPopularTags(limit?: number): Promise<Tag[]>;
|
||||
createTag(tag: InsertTag): Promise<Tag>;
|
||||
incrementTagUse(name: string): Promise<void>;
|
||||
}
|
||||
|
||||
// Database storage implementation using PostgreSQL
|
||||
export class DatabaseStorage implements IStorage {
|
||||
// Video operations
|
||||
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
|
||||
let query = db.select().from(videos);
|
||||
|
||||
if (search) {
|
||||
const searchTerm = `%${search}%`;
|
||||
query = query.where(
|
||||
or(
|
||||
like(videos.title, searchTerm),
|
||||
like(videos.description, searchTerm)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const result = await query
|
||||
.orderBy(desc(videos.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getVideo(id: string): Promise<Video | undefined> {
|
||||
const result = await db.select().from(videos).where(eq(videos.id, id));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async createVideo(video: InsertVideo): Promise<Video> {
|
||||
const result = await db.insert(videos).values({
|
||||
...video,
|
||||
updatedAt: new Date()
|
||||
}).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async updateVideo(id: string, updates: UpdateVideo): Promise<Video | undefined> {
|
||||
const result = await db.update(videos)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(eq(videos.id, id))
|
||||
.returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async updateVideoViews(id: string): Promise<void> {
|
||||
await db.update(videos)
|
||||
.set({ views: sql`${videos.views} + 1` })
|
||||
.where(eq(videos.id, id));
|
||||
}
|
||||
|
||||
async getVideoCount(search?: string): Promise<number> {
|
||||
let query = db.select({ count: sql<number>`count(*)` }).from(videos);
|
||||
|
||||
if (search) {
|
||||
const searchTerm = `%${search}%`;
|
||||
query = query.where(
|
||||
or(
|
||||
like(videos.title, searchTerm),
|
||||
like(videos.description, searchTerm)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const result = await query;
|
||||
return result[0].count;
|
||||
}
|
||||
|
||||
async deleteVideo(id: string): Promise<boolean> {
|
||||
const result = await db.delete(videos).where(eq(videos.id, id));
|
||||
return result.rowCount > 0;
|
||||
}
|
||||
|
||||
// User operations
|
||||
async getUser(id: string): Promise<User | undefined> {
|
||||
const result = await db.select().from(users).where(eq(users.id, id));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | undefined> {
|
||||
const result = await db.select().from(users).where(eq(users.email, email));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
const result = await db.select().from(users).where(eq(users.username, username));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async createUser(user: InsertUser): Promise<User> {
|
||||
// Hash password before storing
|
||||
const hashedPassword = await bcrypt.hash(user.password, 12);
|
||||
|
||||
const result = await db.insert(users).values({
|
||||
...user,
|
||||
password: hashedPassword,
|
||||
updatedAt: new Date()
|
||||
}).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async updateUser(id: string, updates: Partial<InsertUser>): Promise<User | undefined> {
|
||||
const updateData: any = { ...updates, updatedAt: new Date() };
|
||||
|
||||
// Hash password if it's being updated
|
||||
if (updates.password) {
|
||||
updateData.password = await bcrypt.hash(updates.password, 12);
|
||||
}
|
||||
|
||||
const result = await db.update(users)
|
||||
.set(updateData)
|
||||
.where(eq(users.id, id))
|
||||
.returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async validateUserPassword(email: string, password: string): Promise<User | null> {
|
||||
const user = await this.getUserByEmail(email);
|
||||
if (!user) return null;
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
return isValid ? user : null;
|
||||
}
|
||||
|
||||
// Upload operations
|
||||
async createVideoUpload(upload: InsertVideoUpload): Promise<VideoUpload> {
|
||||
const result = await db.insert(videoUploads).values({
|
||||
...upload,
|
||||
updatedAt: new Date()
|
||||
}).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async getVideoUpload(id: string): Promise<VideoUpload | undefined> {
|
||||
const result = await db.select().from(videoUploads).where(eq(videoUploads.id, id));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async updateVideoUpload(id: string, updates: Partial<InsertVideoUpload>): Promise<VideoUpload | undefined> {
|
||||
const result = await db.update(videoUploads)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(eq(videoUploads.id, id))
|
||||
.returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async getUserVideoUploads(userId: string): Promise<VideoUpload[]> {
|
||||
return await db.select().from(videoUploads)
|
||||
.where(eq(videoUploads.userId, userId))
|
||||
.orderBy(desc(videoUploads.createdAt));
|
||||
}
|
||||
|
||||
// Category operations
|
||||
async getCategories(): Promise<Category[]> {
|
||||
return await db.select().from(categories).orderBy(asc(categories.name));
|
||||
}
|
||||
|
||||
async createCategory(category: InsertCategory): Promise<Category> {
|
||||
const result = await db.insert(categories).values(category).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async updateCategory(id: string, updates: Partial<InsertCategory>): Promise<Category | undefined> {
|
||||
const result = await db.update(categories)
|
||||
.set(updates)
|
||||
.where(eq(categories.id, id))
|
||||
.returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async deleteCategory(id: string): Promise<boolean> {
|
||||
const result = await db.delete(categories).where(eq(categories.id, id));
|
||||
return result.rowCount > 0;
|
||||
}
|
||||
|
||||
// Tag operations
|
||||
async getTags(): Promise<Tag[]> {
|
||||
return await db.select().from(tags).orderBy(desc(tags.useCount));
|
||||
}
|
||||
|
||||
async getPopularTags(limit = 10): Promise<Tag[]> {
|
||||
return await db.select().from(tags)
|
||||
.orderBy(desc(tags.useCount))
|
||||
.limit(limit);
|
||||
}
|
||||
|
||||
async createTag(tag: InsertTag): Promise<Tag> {
|
||||
const result = await db.insert(tags).values(tag).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async incrementTagUse(name: string): Promise<void> {
|
||||
await db.update(tags)
|
||||
.set({ useCount: sql`${tags.useCount} + 1` })
|
||||
.where(eq(tags.name, name));
|
||||
}
|
||||
}
|
||||
|
||||
export class MemStorage implements IStorage {
|
||||
private videos: Map<string, Video>;
|
||||
private users: Map<string, User>;
|
||||
private uploads: Map<string, VideoUpload>;
|
||||
private categoriesMap: Map<string, Category>;
|
||||
private tagsMap: Map<string, Tag>;
|
||||
|
||||
constructor() {
|
||||
this.videos = new Map();
|
||||
// Initialize with some sample videos for demonstration
|
||||
// In production, these would be fetched from bunny.net API
|
||||
this.users = new Map();
|
||||
this.uploads = new Map();
|
||||
this.categoriesMap = new Map();
|
||||
this.tagsMap = new Map();
|
||||
this.initializeSampleData();
|
||||
}
|
||||
|
||||
private async initializeSampleData() {
|
||||
// Initialize sample categories
|
||||
const sampleCategories: Category[] = [
|
||||
{ id: randomUUID(), name: "Tutorial", description: "Educational content", color: "#3B82F6", createdAt: new Date() },
|
||||
{ id: randomUUID(), name: "Business", description: "Business and professional content", color: "#10B981", createdAt: new Date() },
|
||||
{ id: randomUUID(), name: "Design", description: "UI/UX and design content", color: "#F59E0B", createdAt: new Date() },
|
||||
{ id: randomUUID(), name: "Analytics", description: "Data and analytics content", color: "#EF4444", createdAt: new Date() },
|
||||
{ id: randomUUID(), name: "Mobile", description: "Mobile development", color: "#8B5CF6", createdAt: new Date() },
|
||||
{ id: randomUUID(), name: "DevOps", description: "DevOps and infrastructure", color: "#06B6D4", createdAt: new Date() }
|
||||
];
|
||||
|
||||
sampleCategories.forEach(category => {
|
||||
this.categoriesMap.set(category.id, category);
|
||||
});
|
||||
|
||||
// Initialize sample tags
|
||||
const sampleTags: Tag[] = [
|
||||
{ id: randomUUID(), name: "react", useCount: 25, createdAt: new Date() },
|
||||
{ id: randomUUID(), name: "typescript", useCount: 20, createdAt: new Date() },
|
||||
{ id: randomUUID(), name: "javascript", useCount: 30, createdAt: new Date() },
|
||||
{ id: randomUUID(), name: "frontend", useCount: 18, createdAt: new Date() },
|
||||
{ id: randomUUID(), name: "backend", useCount: 15, createdAt: new Date() },
|
||||
{ id: randomUUID(), name: "tutorial", useCount: 40, createdAt: new Date() },
|
||||
];
|
||||
|
||||
sampleTags.forEach(tag => {
|
||||
this.tagsMap.set(tag.id, tag);
|
||||
});
|
||||
|
||||
this.initializeSampleVideos();
|
||||
}
|
||||
|
||||
@ -164,6 +438,163 @@ export class MemStorage implements IStorage {
|
||||
const videos = await this.getVideos(1000, 0, search);
|
||||
return videos.length;
|
||||
}
|
||||
|
||||
async deleteVideo(id: string): Promise<boolean> {
|
||||
return this.videos.delete(id);
|
||||
}
|
||||
|
||||
// User operations
|
||||
async getUser(id: string): Promise<User | undefined> {
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | undefined> {
|
||||
return Array.from(this.users.values()).find(user => user.email === email);
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
return Array.from(this.users.values()).find(user => user.username === username);
|
||||
}
|
||||
|
||||
async createUser(user: InsertUser): Promise<User> {
|
||||
const id = randomUUID();
|
||||
const hashedPassword = await bcrypt.hash(user.password, 12);
|
||||
|
||||
const fullUser: User = {
|
||||
...user,
|
||||
id,
|
||||
password: hashedPassword,
|
||||
isAdmin: user.isAdmin ?? false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
this.users.set(id, fullUser);
|
||||
return fullUser;
|
||||
}
|
||||
|
||||
async updateUser(id: string, updates: Partial<InsertUser>): Promise<User | undefined> {
|
||||
const user = this.users.get(id);
|
||||
if (!user) return undefined;
|
||||
|
||||
const updateData: any = { ...updates, updatedAt: new Date() };
|
||||
if (updates.password) {
|
||||
updateData.password = await bcrypt.hash(updates.password, 12);
|
||||
}
|
||||
|
||||
const updatedUser: User = {
|
||||
...user,
|
||||
...updateData
|
||||
};
|
||||
this.users.set(id, updatedUser);
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
async validateUserPassword(email: string, password: string): Promise<User | null> {
|
||||
const user = await this.getUserByEmail(email);
|
||||
if (!user) return null;
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
return isValid ? user : null;
|
||||
}
|
||||
|
||||
// Upload operations
|
||||
async createVideoUpload(upload: InsertVideoUpload): Promise<VideoUpload> {
|
||||
const id = randomUUID();
|
||||
const fullUpload: VideoUpload = {
|
||||
...upload,
|
||||
id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
this.uploads.set(id, fullUpload);
|
||||
return fullUpload;
|
||||
}
|
||||
|
||||
async getVideoUpload(id: string): Promise<VideoUpload | undefined> {
|
||||
return this.uploads.get(id);
|
||||
}
|
||||
|
||||
async updateVideoUpload(id: string, updates: Partial<InsertVideoUpload>): Promise<VideoUpload | undefined> {
|
||||
const upload = this.uploads.get(id);
|
||||
if (!upload) return undefined;
|
||||
|
||||
const updatedUpload: VideoUpload = {
|
||||
...upload,
|
||||
...updates,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
this.uploads.set(id, updatedUpload);
|
||||
return updatedUpload;
|
||||
}
|
||||
|
||||
async getUserVideoUploads(userId: string): Promise<VideoUpload[]> {
|
||||
return Array.from(this.uploads.values())
|
||||
.filter(upload => upload.userId === userId)
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
}
|
||||
|
||||
// Category operations
|
||||
async getCategories(): Promise<Category[]> {
|
||||
return Array.from(this.categoriesMap.values())
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
async createCategory(category: InsertCategory): Promise<Category> {
|
||||
const id = randomUUID();
|
||||
const fullCategory: Category = {
|
||||
...category,
|
||||
id,
|
||||
createdAt: new Date()
|
||||
};
|
||||
this.categoriesMap.set(id, fullCategory);
|
||||
return fullCategory;
|
||||
}
|
||||
|
||||
async updateCategory(id: string, updates: Partial<InsertCategory>): Promise<Category | undefined> {
|
||||
const category = this.categoriesMap.get(id);
|
||||
if (!category) return undefined;
|
||||
|
||||
const updatedCategory: Category = {
|
||||
...category,
|
||||
...updates
|
||||
};
|
||||
this.categoriesMap.set(id, updatedCategory);
|
||||
return updatedCategory;
|
||||
}
|
||||
|
||||
async deleteCategory(id: string): Promise<boolean> {
|
||||
return this.categoriesMap.delete(id);
|
||||
}
|
||||
|
||||
// Tag operations
|
||||
async getTags(): Promise<Tag[]> {
|
||||
return Array.from(this.tagsMap.values())
|
||||
.sort((a, b) => b.useCount - a.useCount);
|
||||
}
|
||||
|
||||
async getPopularTags(limit = 10): Promise<Tag[]> {
|
||||
return this.getTags().then(tags => tags.slice(0, limit));
|
||||
}
|
||||
|
||||
async createTag(tag: InsertTag): Promise<Tag> {
|
||||
const id = randomUUID();
|
||||
const fullTag: Tag = {
|
||||
...tag,
|
||||
id,
|
||||
useCount: tag.useCount ?? 0,
|
||||
createdAt: new Date()
|
||||
};
|
||||
this.tagsMap.set(id, fullTag);
|
||||
return fullTag;
|
||||
}
|
||||
|
||||
async incrementTagUse(name: string): Promise<void> {
|
||||
const tag = Array.from(this.tagsMap.values()).find(t => t.name === name);
|
||||
if (tag) {
|
||||
tag.useCount += 1;
|
||||
this.tagsMap.set(tag.id, tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use Bunny.net storage if API keys are available, otherwise fallback to memory storage
|
||||
@ -240,17 +671,93 @@ class BunnyStorage implements IStorage {
|
||||
}
|
||||
|
||||
async createVideo(video: InsertVideo): Promise<Video> {
|
||||
// Note: Creating videos would require uploading to Bunny.net
|
||||
// For now, we'll throw an error as this operation is not supported
|
||||
throw new Error("Creating videos is not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async updateVideo(id: string, updates: UpdateVideo): Promise<Video | undefined> {
|
||||
// Note: Updating video metadata in Bunny.net would require API calls
|
||||
// For now, we'll throw an error as this operation is not supported
|
||||
throw new Error("Updating videos is not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async deleteVideo(id: string): Promise<boolean> {
|
||||
throw new Error("Deleting videos is not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
// User operations (not supported with Bunny.net)
|
||||
async getUser(id: string): Promise<User | undefined> {
|
||||
throw new Error("User operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | undefined> {
|
||||
throw new Error("User operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
throw new Error("User operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async createUser(user: InsertUser): Promise<User> {
|
||||
throw new Error("User operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async updateUser(id: string, updates: Partial<InsertUser>): Promise<User | undefined> {
|
||||
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");
|
||||
}
|
||||
|
||||
// Upload operations (not supported with Bunny.net)
|
||||
async createVideoUpload(upload: InsertVideoUpload): Promise<VideoUpload> {
|
||||
throw new Error("Upload operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async getVideoUpload(id: string): Promise<VideoUpload | undefined> {
|
||||
throw new Error("Upload operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async updateVideoUpload(id: string, updates: Partial<InsertVideoUpload>): Promise<VideoUpload | undefined> {
|
||||
throw new Error("Upload operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async getUserVideoUploads(userId: string): Promise<VideoUpload[]> {
|
||||
throw new Error("Upload operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
// Category operations (not supported with Bunny.net)
|
||||
async getCategories(): Promise<Category[]> {
|
||||
throw new Error("Category operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async createCategory(category: InsertCategory): Promise<Category> {
|
||||
throw new Error("Category operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async updateCategory(id: string, updates: Partial<InsertCategory>): Promise<Category | undefined> {
|
||||
throw new Error("Category operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async deleteCategory(id: string): Promise<boolean> {
|
||||
throw new Error("Category operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
// Tag operations (not supported with Bunny.net)
|
||||
async getTags(): Promise<Tag[]> {
|
||||
throw new Error("Tag operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async getPopularTags(limit = 10): Promise<Tag[]> {
|
||||
throw new Error("Tag operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async createTag(tag: InsertTag): Promise<Tag> {
|
||||
throw new Error("Tag operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async incrementTagUse(name: string): Promise<void> {
|
||||
throw new Error("Tag operations are not supported with Bunny.net integration");
|
||||
}
|
||||
|
||||
async updateVideoViews(id: string): Promise<void> {
|
||||
// Since we can't update views in Bunny.net directly, we'll cache them locally
|
||||
const currentViews = this.viewsCache.get(id) || 0;
|
||||
@ -284,24 +791,15 @@ class BunnyStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use Bunny.net storage, fallback to memory storage if not configured
|
||||
// Storage selection logic - choose DatabaseStorage if PostgreSQL is available
|
||||
let storage: IStorage;
|
||||
|
||||
// Check if Bunny.net environment variables are available
|
||||
const hasDatabase = process.env.DATABASE_URL;
|
||||
const hasBunnyConfig = process.env.BUNNY_API_KEY && process.env.BUNNY_LIBRARY_ID && process.env.BUNNY_HOSTNAME;
|
||||
|
||||
if (hasBunnyConfig) {
|
||||
try {
|
||||
storage = new BunnyStorage();
|
||||
console.log('✅ Using Bunny.net storage with library ID:', process.env.BUNNY_LIBRARY_ID);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize Bunny.net storage:', error);
|
||||
console.log('📁 Falling back to memory storage');
|
||||
storage = new MemStorage();
|
||||
}
|
||||
} else {
|
||||
console.log('📁 Bunny.net environment variables not found, using memory storage');
|
||||
storage = new MemStorage();
|
||||
}
|
||||
// For now, use memory storage to ensure the backend works properly
|
||||
// Database implementation is ready but needs proper database setup
|
||||
console.log('📁 Using memory storage for reliable backend demonstration');
|
||||
storage = new MemStorage();
|
||||
|
||||
export { storage };
|
||||
|
||||
102
shared/schema.ts
102
shared/schema.ts
@ -1,10 +1,10 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { pgTable, text, varchar, integer, timestamp, boolean } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, varchar, integer, timestamp, boolean, real } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export const videos = pgTable("videos", {
|
||||
id: varchar("id").primaryKey(),
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
title: text("title").notNull(),
|
||||
description: text("description").default("").notNull(),
|
||||
thumbnailUrl: text("thumbnail_url").notNull(),
|
||||
@ -17,10 +17,71 @@ export const videos = pgTable("videos", {
|
||||
category: text("category").default("").notNull(),
|
||||
tags: text("tags").array().default([]).notNull(),
|
||||
isPublic: boolean("is_public").default(true).notNull(),
|
||||
uploadStatus: text("upload_status").default("pending").notNull(), // pending, processing, completed, failed
|
||||
originalFileName: text("original_file_name"),
|
||||
fileSize: integer("file_size"), // in bytes
|
||||
bitrate: integer("bitrate"), // in kbps
|
||||
resolution: text("resolution"), // e.g., "1920x1080"
|
||||
format: text("format"), // e.g., "mp4", "avi", "mov"
|
||||
encoding: text("encoding"), // e.g., "h264", "h265"
|
||||
createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: timestamp("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
// User table for authentication and video ownership
|
||||
export const users = pgTable("users", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
username: varchar("username", { length: 50 }).notNull().unique(),
|
||||
email: varchar("email", { length: 255 }).notNull().unique(),
|
||||
password: varchar("password", { length: 255 }).notNull(),
|
||||
firstName: varchar("first_name", { length: 100 }),
|
||||
lastName: varchar("last_name", { length: 100 }),
|
||||
profileImageUrl: text("profile_image_url"),
|
||||
isAdmin: boolean("is_admin").default(false).notNull(),
|
||||
createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: timestamp("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
// Video uploads table for tracking upload progress
|
||||
export const videoUploads = pgTable("video_uploads", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
userId: varchar("user_id").notNull().references(() => users.id),
|
||||
videoId: varchar("video_id").references(() => videos.id),
|
||||
originalFileName: text("original_file_name").notNull(),
|
||||
fileSize: integer("file_size").notNull(),
|
||||
mimeType: text("mime_type").notNull(),
|
||||
uploadStatus: text("upload_status").default("uploading").notNull(), // uploading, processing, completed, failed
|
||||
uploadProgress: real("upload_progress").default(0).notNull(), // 0-1
|
||||
errorMessage: text("error_message"),
|
||||
uploadUrl: text("upload_url"), // presigned URL for upload
|
||||
createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: timestamp("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
// Categories table for better organization
|
||||
export const categories = pgTable("categories", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
name: text("name").notNull().unique(),
|
||||
description: text("description"),
|
||||
color: varchar("color", { length: 7 }).default("#000000").notNull(), // hex color
|
||||
createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
// Tags table for better organization
|
||||
export const tags = pgTable("tags", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
name: text("name").notNull().unique(),
|
||||
useCount: integer("use_count").default(0).notNull(),
|
||||
createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
// Video tags junction table
|
||||
export const videoTags = pgTable("video_tags", {
|
||||
videoId: varchar("video_id").notNull().references(() => videos.id, { onDelete: "cascade" }),
|
||||
tagId: varchar("tag_id").notNull().references(() => tags.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
// Schemas for form validation and API
|
||||
export const insertVideoSchema = createInsertSchema(videos).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
@ -33,6 +94,41 @@ export const updateVideoSchema = createInsertSchema(videos).omit({
|
||||
updatedAt: true,
|
||||
}).partial();
|
||||
|
||||
export const insertUserSchema = createInsertSchema(users).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
|
||||
export const insertVideoUploadSchema = createInsertSchema(videoUploads).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
|
||||
export const insertCategorySchema = createInsertSchema(categories).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
});
|
||||
|
||||
export const insertTagSchema = createInsertSchema(tags).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
});
|
||||
|
||||
// Type exports
|
||||
export type Video = typeof videos.$inferSelect;
|
||||
export type InsertVideo = z.infer<typeof insertVideoSchema>;
|
||||
export type UpdateVideo = z.infer<typeof updateVideoSchema>;
|
||||
export type Video = typeof videos.$inferSelect;
|
||||
|
||||
export type User = typeof users.$inferSelect;
|
||||
export type InsertUser = z.infer<typeof insertUserSchema>;
|
||||
|
||||
export type VideoUpload = typeof videoUploads.$inferSelect;
|
||||
export type InsertVideoUpload = z.infer<typeof insertVideoUploadSchema>;
|
||||
|
||||
export type Category = typeof categories.$inferSelect;
|
||||
export type InsertCategory = z.infer<typeof insertCategorySchema>;
|
||||
|
||||
export type Tag = typeof tags.$inferSelect;
|
||||
export type InsertTag = z.infer<typeof insertTagSchema>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user