├── .gitignore ├── PRICING.md ├── README.md ├── api ├── controllers │ ├── auth │ │ ├── data.js │ │ ├── logout.js │ │ ├── register.js │ │ ├── signin.js │ │ └── update.js │ └── user.js │ │ ├── addFile.js │ │ └── file.js ├── middleware │ └── auth.js ├── models │ ├── file.js │ └── user.js ├── package-lock.json ├── package.json ├── routes │ ├── file.js │ ├── upload.js │ ├── user.js │ └── webhook.js ├── server.js ├── services │ ├── aws.js │ ├── file.js │ ├── r2.js │ ├── redis.js │ ├── requesform.js │ └── user.js ├── utils │ ├── constants.js │ └── inputvalidation.js └── vercel.json ├── client ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky │ └── pre-commit ├── .prettierrc ├── LICENSE ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── approved.png │ ├── avatar.png │ ├── cloudflare.png │ ├── docker.png │ ├── ecr.png │ ├── ecs.png │ ├── favicon.ico │ ├── google.svg │ ├── hero1.png │ ├── logo.svg │ ├── next.svg │ ├── nodejs.png │ ├── s3.png │ ├── sqs.png │ ├── star.png │ ├── vercel.svg │ ├── verified-badge.png │ └── x-circle.svg ├── src │ ├── api │ │ └── apiInstance.ts │ ├── app │ │ ├── (user) │ │ │ ├── dashboard │ │ │ │ └── page.tsx │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ ├── player │ │ │ │ └── page.tsx │ │ │ ├── profile │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── feature.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── privacy-policy │ │ │ └── page.tsx │ │ └── terms-of-service │ │ │ └── page.tsx │ ├── components │ │ ├── ProtectedRoute.tsx │ │ ├── common │ │ │ ├── avatar-dropdown.tsx │ │ │ ├── footer.tsx │ │ │ └── header.tsx │ │ ├── dashboard │ │ │ ├── DemoListing.tsx │ │ │ ├── DragAndDrop.tsx │ │ │ ├── columns.tsx │ │ │ └── data-table.tsx │ │ ├── home │ │ │ ├── ReviewCard.tsx │ │ │ └── WorkFlowAnimated.tsx │ │ ├── magicui │ │ │ ├── animated-beam.tsx │ │ │ ├── avatar-circles.tsx │ │ │ ├── marquee.tsx │ │ │ └── shine-border.tsx │ │ ├── player │ │ │ ├── VideoDetailsCard.tsx │ │ │ └── VideoPlayer.tsx │ │ └── ui │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── command.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── progress.tsx │ │ │ ├── table.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── typography.tsx │ │ │ └── use-toast.ts │ ├── constants │ │ ├── authitems.ts │ │ └── reviews.ts │ ├── context │ │ └── AuthContext.tsx │ ├── lib │ │ ├── hooks │ │ │ └── useApi.ts │ │ └── utils.ts │ ├── providers │ │ └── theme-provider.tsx │ └── services │ │ └── authService.ts ├── tailwind.config.ts └── tsconfig.json ├── cloudformation.yml └── worker ├── Dockerfile ├── awsHandler ├── config.js ├── constants.js ├── ecshandler.js ├── s3handler.js ├── sqshandler.js ├── taskManager.js └── webhook.js ├── index.js ├── package-lock.json ├── package.json ├── transcode.sh └── utils ├── constants.js └── redis.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore node_modules directory (generated by npm/yarn) 2 | /api/node_modules/ 3 | /worker/node_modules/ 4 | node_modules/ 5 | 6 | /test/ 7 | # Ignore build artifacts and distribution directories 8 | /build/ 9 | /dist/ 10 | /out/ 11 | /.next/ 12 | /.nuxt/ 13 | 14 | # Ignore environment variables file (e.g., .env, .env.local, .env.development, etc.) 15 | .env* 16 | 17 | # Ignore logs and runtime data 18 | /logs/ 19 | /temp/ 20 | /.cache/ 21 | /.eslintcache 22 | /.vscode/ 23 | 24 | # Ignore OS-specific files 25 | .DS_Store 26 | Thumbs.db 27 | 28 | # Ignore editor/IDE configuration files 29 | .idea/ 30 | .vscode/ 31 | -------------------------------------------------------------------------------- /PRICING.md: -------------------------------------------------------------------------------- 1 | ## Cost Analysis 2 | 3 | ### AWS Fargate 4 | - **Number of tasks or pods:** 5 | - 33 per day * (730 hours in a month / 24 hours in a day) = 1003.75 per month 6 | 7 | - **Average duration:** 8 | - 22 minutes = 0.37 hours 9 | 10 | - **Cost Calculation:** 11 | - vCPU hours: 12 | - 1,003.75 tasks x 4 vCPU x 0.37 hours x $0.04048 per hour = $60.14 13 | - GB hours: 14 | - 1,003.75 tasks x 8.00 GB x 0.37 hours x $0.004445 per GB per hour = $13.21 15 | - Ephemeral storage: 16 | - 20 GB - 20 GB (no additional charge) = 0.00 GB billable ephemeral storage per task 17 | 18 | - **Total Cost:** 19 | - $60.14 for vCPU hours + $13.21 for GB hours = $73.35 20 | - Fargate cost (monthly): $73.35 for 1000 videos a month 21 | 22 | ### AWS EC2 23 | - **Savings Plans vs. On-Demand Instances:** 24 | - For utilization over the breakeven point (459.974490 hours), EC2 Instance Savings Plans are more cost-effective than On-Demand Instances. 25 | 26 | - **Savings Plans Cost:** 27 | - Upfront cost: $0.00 28 | - 1 instance x 730 hours in a month = 730 EC2 Instance Savings Plans instance hours per month 29 | - 730 EC2 Instance Savings Plans instance hours per month x $0.098800 = $72.12 30 | 31 | - **On-Demand Instances Cost:** 32 | - 0 On-Demand instance hours per month x $0.156800 = $0.00 33 | 34 | - **Total Cost:** 35 | - $0.00 (On-Demand) + $72.12 (Savings Plans) = $72.12 36 | - *Note: You will pay an hourly commitment for Savings Plans, and your usage will be accrued at a discounted rate against this commitment.* 37 | 38 | ### AWS S3 39 | - **Cost:** 40 | - Negligible as files are deleted after transcoding 41 | 42 | ### AWS SQS 43 | - **Pricing:** 44 | - Free for the first 1 million requests 45 | - $0.40 per million requests for standard queues 46 | - $0.50 per million requests for FIFO queues 47 | 48 | ### Cloudflare R2 49 | - **Costs:** 50 | - Cost per GB/month: $0.015 51 | - Cost for 1TB/month: $15 52 | - Cost of Class A Operations (PUT, COPY): First million requests/month free or $4.50 per million requests 53 | - Cost for 3 million Class A Operations requests: $9 54 | - Cost of Class B Operations (READ): First 10 million requests/month free or $0.36 per million requests 55 | 56 | - **Assumptions:** 57 | - Each 1-hour video = 400 segments 58 | - 400 x 1000 = 400,000 segments 59 | - Each segment is streamed 100 times/month = 400,000 x 100 = 40,000,000 reads 60 | 61 | - **Cost Calculation:** 62 | - Cost for Class B Operations reads: (40,000,000 - 10,000,000) / 1,000,000 x $0.36 = $10.80 63 | 64 | - **Total Cost:** 65 | - $15 (storage) + $9 (Class A Operations) + $10.80 (Class B Operations) = $34.80 66 | 67 | ### Total Cost So Far 68 | - $72.12 (EC2) + $34.80 (Cloudflare R2) = $106.92 69 | 70 | --- 71 | 72 | ### Cheapest Transcoding Service 73 | - **Cost:** 74 | - Per minute: $0.01500 75 | - Per 1-hour video: $0.90 76 | - For 1000 videos: $0.90 x 1000 = $900 77 | 78 | ### AWS Elastic Transcoder 79 | - **Cost:** 80 | - Per minute: $0.03 81 | - Per hour: $0.03 x 60 = $1.80 82 | - For 1000 videos: $1.80 x 1000 = $1800 83 | 84 | *Note: The cost of AWS Elastic Transcoder exceeds the budget by approximately 59 times for just transcoding.* 85 | -------------------------------------------------------------------------------- /api/controllers/auth/data.js: -------------------------------------------------------------------------------- 1 | const { getUserById } = require("../../services/user"); 2 | 3 | async function userDataController(req, res) { 4 | try { 5 | const userId = req.userId; 6 | const user = await getUserById(userId); 7 | if (!user) { 8 | return res.status(404).json({ 9 | status: "error", 10 | code: "USER_NOT_FOUND", 11 | message: "User not found", 12 | }); 13 | } 14 | 15 | return res.status(200).json({ 16 | status: "success", 17 | data: { 18 | user: { 19 | id: user._id, 20 | username: user.username, 21 | email: user.email, 22 | fullname: user.fullname, 23 | message: user.message, 24 | picture: user.picture, 25 | }, 26 | }, 27 | }); 28 | } catch (error) { 29 | console.error("Get user error:", error); 30 | return res.status(500).json({ 31 | status: "error", 32 | code: "INTERNAL_SERVER_ERROR", 33 | message: "An unexpected error occurred while fetching user data.", 34 | }); 35 | } 36 | } 37 | 38 | module.exports = userDataController; 39 | -------------------------------------------------------------------------------- /api/controllers/auth/logout.js: -------------------------------------------------------------------------------- 1 | async function logoutController(req, res) { 2 | try { 3 | res.clearCookie("token"); 4 | return res.status(200).json({ 5 | status: "success", 6 | message: "Logged out successfully", 7 | }); 8 | } catch (error) { 9 | console.error("Logout error:", error); 10 | return res.status(500).json({ 11 | status: "error", 12 | code: "INTERNAL_SERVER_ERROR", 13 | message: "An unexpected error occurred during logout.", 14 | }); 15 | } 16 | } 17 | 18 | module.exports = logoutController; 19 | -------------------------------------------------------------------------------- /api/controllers/auth/register.js: -------------------------------------------------------------------------------- 1 | const { 2 | userExist, 3 | createUser, 4 | passwordEncrypt, 5 | } = require("../../services/user"); 6 | const { registerSchema } = require("../../utils/inputvalidation"); 7 | 8 | async function registerController(req, res) { 9 | try { 10 | const { error, value } = registerSchema.validate(req.body); 11 | if (error) { 12 | return res.status(400).json({ 13 | status: "error", 14 | code: "VALIDATION_ERROR", 15 | message: error.details[0].message, 16 | }); 17 | } 18 | 19 | const { username, password, email, message, fullname } = value; 20 | const userExists = await userExist(username, email); 21 | if (userExists) { 22 | return res.status(409).json({ 23 | status: "error", 24 | code: "USER_ALREADY_EXISTS", 25 | message: "A user with this username or email already exists", 26 | }); 27 | } 28 | 29 | const hashedPassword = await passwordEncrypt(password); 30 | await createUser(username, hashedPassword, email, message, fullname); 31 | 32 | return res.status(201).json({ 33 | status: "success", 34 | message: "User registered successfully", 35 | }); 36 | } catch (error) { 37 | console.error("Registration error:", error); 38 | return res.status(500).json({ 39 | status: "error", 40 | code: "INTERNAL_SERVER_ERROR", 41 | message: "An unexpected error occurred. Please try again later.", 42 | }); 43 | } 44 | } 45 | 46 | module.exports = registerController; 47 | -------------------------------------------------------------------------------- /api/controllers/auth/signin.js: -------------------------------------------------------------------------------- 1 | const { verifyPassword } = require("../../services/user"); 2 | const { getUserByUsername, generateAuthToken } = require("../../services/user"); 3 | const { loginSchema } = require("../../utils/inputvalidation"); 4 | 5 | async function signInController(req, res) { 6 | try { 7 | const { error, value } = loginSchema.validate(req.body); 8 | if (error) { 9 | return res.status(400).json({ 10 | status: "error", 11 | code: "VALIDATION_ERROR", 12 | message: error.details[0].message, 13 | }); 14 | } 15 | 16 | const { username, password } = value; 17 | const user = await getUserByUsername(username); 18 | 19 | if (!user) { 20 | return res.status(401).json({ 21 | status: "error", 22 | code: "INVALID_CREDENTIALS", 23 | message: "Invalid username or password", 24 | }); 25 | } 26 | 27 | const isPasswordValid = await verifyPassword(password, user.password); 28 | if (!isPasswordValid) { 29 | return res.status(401).json({ 30 | status: "error", 31 | code: "INVALID_CREDENTIALS", 32 | message: "Invalid username or password", 33 | }); 34 | } 35 | 36 | const token = await generateAuthToken(user._id); 37 | 38 | res.cookie("token", token, { 39 | httpOnly: true, 40 | secure: process.env.NODE_ENV === "production", 41 | }); 42 | 43 | return res.status(200).json({ 44 | status: "success", 45 | message: "Login successful", 46 | data: { 47 | userId: user._id, 48 | }, 49 | }); 50 | } catch (error) { 51 | console.error("Login error:", error); 52 | return res.status(500).json({ 53 | status: "error", 54 | code: "INTERNAL_SERVER_ERROR", 55 | message: "An unexpected error occurred. Please try again later.", 56 | }); 57 | } 58 | } 59 | 60 | module.exports = signInController; 61 | -------------------------------------------------------------------------------- /api/controllers/auth/update.js: -------------------------------------------------------------------------------- 1 | const { updateUser } = require("../../services/user"); 2 | const { updateUserSchema } = require("../../utils/inputvalidation"); 3 | 4 | async function updateUserController(req, res) { 5 | try { 6 | const { error, value } = updateUserSchema.validate(req.body); 7 | if (error) { 8 | return res.status(400).json({ 9 | status: "error", 10 | code: "VALIDATION_ERROR", 11 | message: error.details[0].message, 12 | }); 13 | } 14 | 15 | const { username, fullname, picture, message } = value; 16 | const userId = req.userId; 17 | const user = await updateUser(userId, username, fullname, picture, message); 18 | 19 | return res.status(user.status).json({ 20 | status: user.status === 200 ? "success" : "error", 21 | message: user.message, 22 | }); 23 | } catch (error) { 24 | console.error("Update user error:", error); 25 | return res.status(500).json({ 26 | status: "error", 27 | code: "INTERNAL_SERVER_ERROR", 28 | message: "An unexpected error occurred while updating user data.", 29 | }); 30 | } 31 | } 32 | 33 | module.exports = updateUserController; 34 | -------------------------------------------------------------------------------- /api/controllers/user.js/addFile.js: -------------------------------------------------------------------------------- 1 | const { addFile } = require("../../services/file"); 2 | const { isAllowedToUpload } = require("../../services/user"); 3 | const { addFileSchema } = require("../../utils/inputValidation"); 4 | 5 | async function addFileController(req, res) { 6 | try { 7 | const { error, value } = addFileSchema.validate(req.body); 8 | if (error) { 9 | return res.status(400).json({ 10 | status: "error", 11 | code: "VALIDATION_ERROR", 12 | message: error.details[0].message, 13 | }); 14 | } 15 | 16 | const userId = req.userId; 17 | const isAllowed = await isAllowedToUpload(userId); 18 | if (!isAllowed) { 19 | return res.status(403).json({ 20 | status: "error", 21 | code: "UPLOAD_NOT_ALLOWED", 22 | message: "You are not allowed to add files. Please contact admin.", 23 | }); 24 | } 25 | 26 | const { fileName, size, type } = value; 27 | const sanitizedFileName = fileName.replace(/ /g, ""); 28 | const filePath = `${userId}/${sanitizedFileName}`; 29 | 30 | const file = await addFile( 31 | sanitizedFileName, 32 | filePath, 33 | size, 34 | type, 35 | userId, 36 | "queued" 37 | ); 38 | 39 | return res.status(201).json({ 40 | status: "success", 41 | message: "File added successfully", 42 | data: { fileId: file._id }, 43 | }); 44 | } catch (error) { 45 | console.error("Error adding file:", error); 46 | return res.status(500).json({ 47 | status: "error", 48 | code: "INTERNAL_SERVER_ERROR", 49 | message: "An unexpected error occurred while adding the file.", 50 | }); 51 | } 52 | } 53 | 54 | module.exports = addFileController; 55 | -------------------------------------------------------------------------------- /api/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | const protectedRoute = (req, res, next) => { 4 | const { token } = req.cookies; 5 | if (!token) { 6 | return res.status(401).json({ 7 | message: "Not authorized", 8 | status: 401, 9 | }); 10 | } 11 | try { 12 | const { userId } = jwt.verify(token, process.env.SECRET_KEY); 13 | req.userId = userId; 14 | next(); 15 | } catch (error) { 16 | return res.status(401).json({ 17 | message: "Invalid token", 18 | status: 401, 19 | }); 20 | } 21 | }; 22 | 23 | module.exports = { 24 | protectedRoute, 25 | }; 26 | -------------------------------------------------------------------------------- /api/models/file.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | 4 | const fileSchema = new mongoose.Schema({ 5 | name:{ 6 | type:String, 7 | required:[true, "File name is required"] 8 | }, 9 | url:{ 10 | type:String, 11 | }, 12 | size:{ 13 | type:Number, 14 | required:[true, "File size is required"] 15 | }, 16 | path:{ 17 | type:String, 18 | required:[true, "File path is required"] 19 | }, 20 | type:{ 21 | type:String, 22 | required:[true, "File mimetype is required"] 23 | }, 24 | status:{ 25 | type:String, 26 | required:[true, "File status is required"], 27 | }, 28 | user:{ 29 | type:mongoose.Schema.Types.ObjectId, 30 | ref:'User' 31 | }, 32 | hlsurl:{ 33 | type:String 34 | }, 35 | fileurl:{ 36 | type:String 37 | } 38 | }, { timestamps: true }); 39 | 40 | const File =mongoose.model('File', fileSchema); 41 | 42 | module.exports = {File}; -------------------------------------------------------------------------------- /api/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const userSchema = new mongoose.Schema({ 4 | username: { 5 | type: String, 6 | required: [true, "Username is required"], 7 | unique: true, 8 | }, 9 | fullname: { 10 | type: String, 11 | required: [true, "Fullname is required"], 12 | }, 13 | password: { 14 | type: String, 15 | required: [true, "Password is required"], 16 | }, 17 | email: { 18 | type: String, 19 | required: [true, "Email is required"], 20 | unique: true, 21 | }, 22 | picture: { 23 | type: String, 24 | default: "", 25 | }, 26 | isAllowed: { 27 | type: Boolean, 28 | default: true, 29 | }, 30 | message: { 31 | type: String, 32 | required: [true, "Message is required"], 33 | }, 34 | files: [ 35 | { 36 | type: mongoose.Schema.Types.ObjectId, 37 | ref: "File", 38 | }, 39 | ], 40 | }); 41 | 42 | const User = mongoose.model("User", userSchema); 43 | 44 | module.exports = { User }; 45 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "node server.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@aws-sdk/client-s3": "^3.598.0", 14 | "@aws-sdk/s3-request-presigner": "^3.600.0", 15 | "@upstash/redis": "^1.31.5", 16 | "aws-sdk": "^2.1643.0", 17 | "bcrypt": "^5.1.1", 18 | "cookie-parser": "^1.4.6", 19 | "cors": "^2.8.5", 20 | "dotenv": "^16.4.5", 21 | "express": "^4.19.2", 22 | "joi": "^17.13.3", 23 | "jsonwebtoken": "^9.0.2", 24 | "mongoose": "^8.4.1", 25 | "multer": "^1.4.5-lts.1", 26 | "uuid": "^10.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /api/routes/file.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { protectedRoute } = require("../middleware/auth"); 3 | const { 4 | getFileController, 5 | getHlsUrlController, 6 | getFilesController, 7 | deleteFileController, 8 | } = require("../controllers/user.js/file"); 9 | const router = express.Router(); 10 | 11 | router.get("/getfile", protectedRoute, getFileController); 12 | 13 | router.get("/hlsurl", protectedRoute, getHlsUrlController); 14 | 15 | router.get("/getfiles", protectedRoute, getFilesController); 16 | 17 | router.delete("/delete", protectedRoute, deleteFileController); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /api/routes/upload.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { protectedRoute } = require("../middleware/auth"); 3 | const { 4 | getUploadUrlController, 5 | addFileController, 6 | } = require("../controllers/user.js/file"); 7 | 8 | const router = express.Router(); 9 | 10 | router.get("/url", protectedRoute, getUploadUrlController); 11 | 12 | router.post("/add", protectedRoute, addFileController); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /api/routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { protectedRoute } = require("../middleware/auth"); 3 | const registerController = require("../controllers/auth/register"); 4 | const signInController = require("../controllers/auth/signin"); 5 | const logoutController = require("../controllers/auth/logout"); 6 | const userDataController = require("../controllers/auth/data"); 7 | const updateUserController = require("../controllers/auth/update"); 8 | 9 | const router = express.Router(); 10 | 11 | router.post("/signup", registerController); 12 | 13 | router.post("/login", signInController); 14 | 15 | router.post("/logout", logoutController); 16 | 17 | router.get("/me", protectedRoute, userDataController); 18 | 19 | router.patch("/update", protectedRoute, updateUserController); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /api/routes/webhook.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { updateFileStatusController } = require("../controllers/user.js/file"); 3 | 4 | const router = express.Router(); 5 | 6 | router.post("/updatestatus", updateFileStatusController); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /api/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cookieParser = require("cookie-parser"); 3 | const uploadRouter = require("./routes/upload"); 4 | const userRouter = require("./routes/user"); 5 | const cors = require("cors"); 6 | const mongoose = require("mongoose"); 7 | const webhookRouter = require("./routes/webhook"); 8 | const fileRouter = require("./routes/file"); 9 | require("dotenv").config(); 10 | 11 | const app = express(); 12 | app.use(express.json()); 13 | app.use( 14 | cors({ 15 | credentials: true, 16 | origin: [ 17 | "http://localhost:5173", 18 | "http://localhost:3000", 19 | "https://streamscale.aksdev.me", 20 | ], 21 | }) 22 | ); 23 | app.use(cookieParser()); 24 | 25 | app.use("/api/upload", uploadRouter); 26 | app.use("/api/user", userRouter); 27 | app.use("/api/webhook", webhookRouter); 28 | app.use("/api/file", fileRouter); 29 | 30 | mongoose.connect(process.env.MONGO_URI); 31 | 32 | const port = 3002; 33 | 34 | app.listen(port, () => { 35 | console.log(`Server is running on port ${port}`); 36 | }); 37 | -------------------------------------------------------------------------------- /api/services/aws.js: -------------------------------------------------------------------------------- 1 | const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); 2 | const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); 3 | require("dotenv").config(); 4 | 5 | const s3Client = new S3Client({ 6 | region: process.env.AWS_REGION, 7 | credentials: { 8 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 9 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 10 | }, 11 | }); 12 | 13 | function sanitizePath(path) { 14 | return path.replace(/[^a-zA-Z0-9_.\/-]/g, ""); 15 | } 16 | 17 | async function generatePresignedUrl(path) { 18 | const sanitizedPath = sanitizePath(path); 19 | 20 | try { 21 | const command = new PutObjectCommand({ 22 | Bucket: process.env.AWS_S3_BUCKET, 23 | Key: sanitizedPath, 24 | }); 25 | 26 | const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); 27 | return url; 28 | } catch (error) { 29 | console.error("Error generating presigned URL:", error); 30 | throw new Error("Failed to generate presigned URL"); 31 | } 32 | } 33 | 34 | module.exports = { 35 | generatePresignedUrl, 36 | }; 37 | -------------------------------------------------------------------------------- /api/services/file.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { File } = require("../models/file"); 3 | const { User } = require("../models/user"); 4 | const { deleteR2Directory } = require("./r2"); 5 | 6 | const sanitizePath = (filePath) => filePath.replace(/[^a-zA-Z0-9_ .\/-]/g, ""); 7 | 8 | const addFile = async (filename, filePath, size, type, userId, status) => { 9 | try { 10 | const file = new File({ 11 | name: filename, 12 | size, 13 | type, 14 | status, 15 | user: userId, 16 | path: sanitizePath(filePath), 17 | }); 18 | await file.save(); 19 | await User.findByIdAndUpdate(userId, { $push: { files: file._id } }); 20 | return file; 21 | } catch (error) { 22 | console.error("Error adding file:", error); 23 | throw new Error("Failed to add file"); 24 | } 25 | }; 26 | 27 | const getFileByPath = async (filePath) => { 28 | try { 29 | return await File.findOne({ path: filePath }); 30 | } catch (error) { 31 | console.error("Error getting file by path:", error); 32 | throw new Error("Failed to get file by path"); 33 | } 34 | }; 35 | 36 | const getFile = async (fileId) => { 37 | try { 38 | return await File.findById(fileId); 39 | } catch (error) { 40 | console.error("Error getting file:", error); 41 | throw new Error("Failed to get file"); 42 | } 43 | }; 44 | 45 | const deleteFile = async (fileId) => { 46 | try { 47 | const file = await File.findOneAndDelete({ _id: fileId }); 48 | if (!file) { 49 | throw new Error("File not found"); 50 | } 51 | await User.updateOne({ _id: file.user }, { $pull: { files: fileId } }); 52 | await deleteR2Directory(file.path); 53 | } catch (error) { 54 | console.error("Error deleting file:", error); 55 | throw new Error("Failed to delete file"); 56 | } 57 | }; 58 | 59 | const updateFileStatusByPath = async (filePath, status) => { 60 | if (!filePath || !status) { 61 | throw new Error("Path or status is missing"); 62 | } 63 | try { 64 | const file = await File.findOneAndUpdate( 65 | { path: filePath }, 66 | { status }, 67 | { new: true } 68 | ); 69 | if (!file) { 70 | throw new Error("File not found"); 71 | } 72 | return file; 73 | } catch (error) { 74 | console.error("Error updating file status by path:", error); 75 | throw new Error("Failed to update file status"); 76 | } 77 | }; 78 | 79 | const updateFileStatus = async (fileId, status) => { 80 | try { 81 | const file = await File.findByIdAndUpdate( 82 | fileId, 83 | { status }, 84 | { new: true } 85 | ); 86 | if (!file) { 87 | throw new Error("File not found"); 88 | } 89 | return file; 90 | } catch (error) { 91 | console.error("Error updating file status:", error); 92 | throw new Error("Failed to update file status"); 93 | } 94 | }; 95 | 96 | const getFiles = async (userId) => { 97 | try { 98 | const user = await User.findById(userId).populate({ 99 | path: "files", 100 | select: "name size type status createdAt", 101 | }); 102 | if (!user) { 103 | throw new Error("User not found"); 104 | } 105 | return user.files; 106 | } catch (error) { 107 | console.error("Error getting files:", error); 108 | throw new Error("Failed to get files"); 109 | } 110 | }; 111 | 112 | const getHlsUrl = async (key) => { 113 | try { 114 | const dir = path.dirname(key); 115 | const base = path.basename(key, path.extname(key)); 116 | const pathDir = path.join(dir, base).replace(/\\/g, "/"); 117 | const hlsUrl = `${process.env.CLOUDFLARE_PUBLIC_BASE_URL}/${pathDir}/master.m3u8`; 118 | return hlsUrl || null; 119 | } catch (error) { 120 | console.error("Error generating HLS URL:", error); 121 | throw new Error("Failed to generate HLS URL"); 122 | } 123 | }; 124 | 125 | module.exports = { 126 | addFile, 127 | getFiles, 128 | deleteFile, 129 | updateFileStatus, 130 | getFile, 131 | updateFileStatusByPath, 132 | getFileByPath, 133 | getHlsUrl, 134 | }; 135 | -------------------------------------------------------------------------------- /api/services/r2.js: -------------------------------------------------------------------------------- 1 | const { 2 | S3Client, 3 | ListObjectsV2Command, 4 | DeleteObjectsCommand, 5 | } = require("@aws-sdk/client-s3"); 6 | const dotenv = require("dotenv"); 7 | const path = require("path"); 8 | const { r2BucketName } = require("../utils/constants"); 9 | 10 | dotenv.config(); 11 | 12 | const s3Client = new S3Client({ 13 | region: process.env.R2_REGION, 14 | endpoint: process.env.R2_ENDPOINT, 15 | credentials: { 16 | accessKeyId: process.env.R2_ACCESS_KEY_ID, 17 | secretAccessKey: process.env.R2_SECRET_ACCESS_KEY, 18 | }, 19 | }); 20 | 21 | async function getFileDirectory(key) { 22 | try { 23 | const dir = path.dirname(key); 24 | const base = path.basename(key, path.extname(key)); 25 | const pathdir = path.join(dir, base).replace(/\\/g, "/"); 26 | return pathdir; 27 | } catch (error) { 28 | console.error(error); 29 | throw error; 30 | } 31 | } 32 | 33 | async function deleteR2Directory(key) { 34 | try { 35 | const directoryPrefix = await getFileDirectory(key); 36 | const listCommand = new ListObjectsV2Command({ 37 | Bucket: r2BucketName, 38 | Prefix: directoryPrefix, 39 | }); 40 | const listedObjects = await s3Client.send(listCommand); 41 | if (!listedObjects.Contents) { 42 | return; 43 | } 44 | const fileKeys = listedObjects.Contents.map((file) => file.Key); 45 | const deleteParams = { 46 | Bucket: r2BucketName, 47 | Delete: { 48 | Objects: fileKeys.map((key) => ({ Key: key })), 49 | }, 50 | }; 51 | const command = new DeleteObjectsCommand(deleteParams); 52 | await s3Client.send(command); 53 | } catch (error) { 54 | console.error(error); 55 | throw error; 56 | } 57 | } 58 | 59 | module.exports = { deleteR2Directory }; 60 | -------------------------------------------------------------------------------- /api/services/redis.js: -------------------------------------------------------------------------------- 1 | const { Redis } = require("@upstash/redis"); 2 | const { redisUrl, redisToken } = require("../utils/constants"); 3 | 4 | const upstashRedis = new Redis({ 5 | url: redisUrl, 6 | token: redisToken, 7 | }); 8 | 9 | const updateStatus = async (key, value) => { 10 | await upstashRedis.set(key, value); 11 | }; 12 | 13 | const getStatus = async (key) => { 14 | return await upstashRedis.get(key); 15 | }; 16 | 17 | const deleteStatus = async (key) => { 18 | await upstashRedis.del(key); 19 | }; 20 | 21 | module.exports = { 22 | updateStatus, 23 | getStatus, 24 | deleteStatus, 25 | }; 26 | -------------------------------------------------------------------------------- /api/services/requesform.js: -------------------------------------------------------------------------------- 1 | const { Requestform } = require("../models/requestform"); 2 | 3 | //not being used right now 4 | const createRequestform = async (email, message) => { 5 | try { 6 | const requestform = new Requestform({ email, message }); 7 | await requestform.save(); 8 | return requestform; 9 | } catch (error) { 10 | console.error(error); 11 | throw error; 12 | } 13 | }; 14 | 15 | module.exports = { createRequestform }; 16 | -------------------------------------------------------------------------------- /api/services/user.js: -------------------------------------------------------------------------------- 1 | const { User } = require("../models/user"); 2 | const jwt = require("jsonwebtoken"); 3 | const bcrypt = require("bcrypt"); 4 | const saltRounds = 10; 5 | 6 | const createUser = async (username, password, email, message, fullname) => { 7 | try { 8 | const user = new User({ username, password, email, message, fullname }); 9 | await user.save(); 10 | return user; 11 | } catch (error) { 12 | console.error("Error creating user", error); 13 | } 14 | }; 15 | 16 | const getUserById = async (userId) => { 17 | try { 18 | const user = await User.findById(userId, { password: 0, files: 0 }); 19 | if (user) return user; 20 | return null; 21 | } catch (error) { 22 | console.error("Error getting user", error); 23 | } 24 | }; 25 | 26 | const generateAuthToken = async (userId) => { 27 | const token = jwt.sign( 28 | { userId: userId.toString() }, 29 | process.env.SECRET_KEY, 30 | { 31 | expiresIn: "1h", 32 | } 33 | ); 34 | return token; 35 | }; 36 | 37 | const getUserByUsername = async (username) => { 38 | try { 39 | const user = await User.findOne({ username }); 40 | if (user) return user; 41 | return null; 42 | } catch (error) { 43 | console.error("Error getting user", error); 44 | } 45 | }; 46 | 47 | const userExist = async (username, email) => { 48 | try { 49 | const userExist = await User.findOne({ 50 | $or: [{ username }, { email }], 51 | }); 52 | return !!userExist; 53 | } catch (error) { 54 | console.error("Error checking if user exists", error); 55 | } 56 | }; 57 | 58 | const isAllowedToUpload = async (userId) => { 59 | try { 60 | const user = await User.findById(userId); 61 | return user.isAllowed; 62 | } catch (error) { 63 | console.error("Error checking if user is allowed to upload", error); 64 | } 65 | }; 66 | 67 | const updateUser = async (userId, username, fullname, picture, message) => { 68 | try { 69 | const emailExist = await userExist(username, null); 70 | if (emailExist) { 71 | return { 72 | message: "Username already exists", 73 | status: 400, 74 | }; 75 | } 76 | const user = await User.findOneAndUpdate( 77 | { _id: userId }, 78 | { username, fullname, picture, message } 79 | ); 80 | if (!user) { 81 | return { 82 | message: "Could not update user details", 83 | status: 400, 84 | }; 85 | } 86 | return { 87 | message: "Successfully updated user", 88 | status: 200, 89 | }; 90 | } catch (error) { 91 | console.error("Error updating user", error); 92 | } 93 | }; 94 | 95 | const passwordEncrypt = async (password) => { 96 | try { 97 | const hashedpassword = await bcrypt.hash(password, saltRounds); 98 | return hashedpassword; 99 | } catch (error) { 100 | console.error("Error hashing the password", error); 101 | } 102 | }; 103 | 104 | const verifyPassword = async (password, hashedPassword) => { 105 | try { 106 | const match = await bcrypt.compare(password, hashedPassword); 107 | return match; 108 | } catch (error) { 109 | console.error("Error verifying the password", error); 110 | } 111 | }; 112 | 113 | module.exports = { 114 | createUser, 115 | getUserByUsername, 116 | getUserById, 117 | userExist, 118 | isAllowedToUpload, 119 | updateUser, 120 | generateAuthToken, 121 | passwordEncrypt, 122 | verifyPassword, 123 | }; 124 | -------------------------------------------------------------------------------- /api/utils/constants.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | dotenv.config(); 3 | 4 | const clusterArn = process.env.AWS_CLUSTER_ARN; 5 | const taskArn = process.env.AWS_TASK_ARN; 6 | const queueUrl = process.env.AWS_QUEUE_URL; 7 | const subnetId = [ 8 | process.env.SUBNET1, 9 | process.env.SUBNET2, 10 | process.env.SUBNET3, 11 | ]; 12 | const securityGroupId = process.env.SECURITY_GROUP_ID; 13 | const max_running_tasks = process.env.MAX_RUNNING_TASKS; 14 | const redisUrl = process.env.UPSTASH_REDIS_URL; 15 | const redisToken = process.env.UPSTASH_REDIS_TOKEN; 16 | const r2BucketName = process.env.R2_BUCKET; 17 | 18 | module.exports = { 19 | clusterArn, 20 | taskArn, 21 | queueUrl, 22 | subnetId, 23 | securityGroupId, 24 | max_running_tasks, 25 | redisUrl, 26 | redisToken, 27 | r2BucketName, 28 | }; 29 | -------------------------------------------------------------------------------- /api/utils/inputvalidation.js: -------------------------------------------------------------------------------- 1 | const Joi = require("joi"); 2 | 3 | const registerSchema = Joi.object({ 4 | username: Joi.string().alphanum().min(3).max(30).required(), 5 | fullname: Joi.string().required(), 6 | password: Joi.string().min(6).required(), 7 | email: Joi.string().email().required(), 8 | message: Joi.string().optional().required(), 9 | }); 10 | 11 | const loginSchema = Joi.object({ 12 | username: Joi.string().alphanum().min(3).max(30).required(), 13 | password: Joi.string().min(6).required(), 14 | }); 15 | 16 | const addFileSchema = Joi.object({ 17 | fileName: Joi.string().required(), 18 | size: Joi.number().required(), 19 | type: Joi.string().required(), 20 | }); 21 | 22 | const updateUserSchema = Joi.object({ 23 | username: Joi.string().alphanum().min(3).max(30), 24 | fullname: Joi.string(), 25 | picture: Joi.string(), 26 | message: Joi.string().min(20), 27 | }); 28 | module.exports = { 29 | registerSchema, 30 | loginSchema, 31 | addFileSchema, 32 | updateUserSchema, 33 | }; 34 | -------------------------------------------------------------------------------- /api/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/api" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /client/.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // env: { 3 | // browser: true, 4 | // es2021: true 5 | // }, 6 | extends: [ 7 | // 'standard-with-typescript', 8 | // 'plugin:react/recommended', 9 | // 'prettier', 10 | 'next' 11 | ], 12 | // overrides: [ 13 | // { 14 | // env: { 15 | // node: true 16 | // }, 17 | // files: ['.eslintrc.{js,cjs}'], 18 | // parserOptions: { 19 | // sourceType: 'script' 20 | // } 21 | // } 22 | // ], 23 | // parserOptions: { 24 | // ecmaVersion: 'latest', 25 | // sourceType: 'module', 26 | // project: './tsconfig.json', 27 | // exclude: ['**/*.eslintrc.js'] 28 | // }, 29 | // settings: { 30 | // 'import/resolver': { 31 | // alias: { 32 | // map: [ 33 | // ['@', './src'], 34 | // ['@ui', './src/components/ui'], 35 | // ['@components', './src/components'], 36 | // ['@lib', './src/lib'] 37 | // ], 38 | // extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'] 39 | // } 40 | // }, 41 | // react: { 42 | // version: 'detect' 43 | // } 44 | // }, 45 | // plugins: ['react'], 46 | rules: { 47 | 'react/no-unescaped-entities': 'off', 48 | '@next/next/no-page-custom-font': 'off', 49 | 'react/display-name': 'off' 50 | // 'comma-dangle': 0, 51 | // 'react/jsx-one-expression-per-line': 0, 52 | // 'react/prop-types': 0, 53 | // 'react/jsx-props-no-spreading': 0, 54 | // 'object-curly-newline': 0, 55 | // 'react/jsx-pascal-case': 0, 56 | // 'react/jsx-filename-extension': 0, 57 | // 'react/react-in-jsx-scope': 0, 58 | // 'react/jsx-uses-react': 0, 59 | // '@typescript-eslint/indent': 0, 60 | // '@typescript-eslint/strict-boolean-expressions': 0, 61 | // '@typescript-eslint/explicit-function-return-type': 0, 62 | // '@typescript-eslint/space-before-function-paren': 0 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # env 39 | .env 40 | .env.local 41 | # next-video 42 | videos/* 43 | !videos/*.json 44 | !videos/*.js 45 | !videos/*.ts 46 | public/_next-video 47 | 48 | .vercel 49 | -------------------------------------------------------------------------------- /client/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "printWidth": 60, 6 | "plugins": ["prettier-plugin-classnames"], 7 | "semi": false, 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /client/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anthony Sistilli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withNextVideo } from 'next-video/process' 2 | /** @type {import('next').NextConfig} */ 3 | const nextConfig = { 4 | distDir: 'build', 5 | reactStrictMode: false, 6 | experimental: { 7 | missingSuspenseWithCSRBailout: false 8 | }, 9 | images: { 10 | domains: [ 11 | 'scontent.fyzd1-3.fna.fbcdn.net', 12 | 'instagram.fyto1-2.fna.fbcdn.net', 13 | 'assets.vogue.com', 14 | 'm.media-amazon.com', 15 | 'upload.wikimedia.org', 16 | 'avatars.githubusercontent.com', 17 | 'avatar.vercel.sh' 18 | ] 19 | } 20 | } 21 | 22 | export default withNextVideo(nextConfig) 23 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "landing-page-boilerplate", 3 | "version": "0.1.0", 4 | "author": { 5 | "name": "Aman Kumar", 6 | "url": "https://github.com/amankumarsingh77" 7 | }, 8 | "private": true, 9 | "scripts": { 10 | "dev": "next dev", 11 | "build": "next build", 12 | "start": "next start", 13 | "lint": "npm run next-lint && npm run format", 14 | "next-lint": "next lint --fix --quiet", 15 | "format": "prettier --write --log-level error .", 16 | "prepare": "husky" 17 | }, 18 | "dependencies": { 19 | "@headlessui/react": "^1.7.18", 20 | "@headlessui/tailwindcss": "^0.2.0", 21 | "@hookform/resolvers": "^3.6.0", 22 | "@nextui-org/avatar": "^2.0.30", 23 | "@nextui-org/react": "^2.4.2", 24 | "@nextui-org/tooltip": "^2.0.36", 25 | "@radix-ui/react-avatar": "^1.1.0", 26 | "@radix-ui/react-checkbox": "^1.1.0", 27 | "@radix-ui/react-dialog": "^1.1.1", 28 | "@radix-ui/react-dropdown-menu": "^2.1.1", 29 | "@radix-ui/react-icons": "^1.3.0", 30 | "@radix-ui/react-label": "^2.1.0", 31 | "@radix-ui/react-popover": "^1.0.7", 32 | "@radix-ui/react-progress": "^1.1.0", 33 | "@radix-ui/react-scroll-area": "^1.0.5", 34 | "@radix-ui/react-select": "^2.0.0", 35 | "@radix-ui/react-separator": "^1.0.3", 36 | "@radix-ui/react-slider": "^1.1.2", 37 | "@radix-ui/react-slot": "^1.1.0", 38 | "@radix-ui/react-switch": "^1.0.3", 39 | "@radix-ui/react-toast": "^1.2.1", 40 | "@radix-ui/react-tooltip": "^1.1.2", 41 | "@remixicon/react": "^4.2.0", 42 | "@tanstack/react-query": "^5.45.1", 43 | "@tanstack/react-table": "^8.17.3", 44 | "@vercel/analytics": "^1.2.2", 45 | "axios": "^1.7.2", 46 | "class-variance-authority": "^0.7.0", 47 | "clsx": "^2.1.1", 48 | "cmdk": "^1.0.0", 49 | "cookie": "^0.6.0", 50 | "date-fns": "^3.6.0", 51 | "lucide-react": "^0.330.0", 52 | "next": "14.1.0", 53 | "next-themes": "^0.2.1", 54 | "next-video": "^1.1.0", 55 | "react": "^18", 56 | "react-day-picker": "^8.10.1", 57 | "react-dom": "^18", 58 | "react-hook-form": "^7.52.0", 59 | "tailwind-merge": "^2.3.0", 60 | "tailwindcss": "^3.4.4", 61 | "tailwindcss-animate": "^1.0.7", 62 | "vaul": "^0.9.0", 63 | "zod": "^3.23.8" 64 | }, 65 | "devDependencies": { 66 | "@tailwindcss/forms": "^0.5.7", 67 | "@types/cookie": "^0.6.0", 68 | "@types/node": "^20", 69 | "@types/react": "^18", 70 | "@types/react-dom": "^18", 71 | "@types/uuid": "^9.0.8", 72 | "@typescript-eslint/eslint-plugin": "^6.21.0", 73 | "autoprefixer": "^10.0.1", 74 | "eslint": "^8.56.0", 75 | "eslint-config-airbnb": "^19.0.4", 76 | "eslint-config-next": "^14.1.0", 77 | "eslint-config-prettier": "^9.1.0", 78 | "eslint-config-standard-with-typescript": "^43.0.1", 79 | "eslint-import-resolver-alias": "^1.1.2", 80 | "eslint-plugin-import": "^2.29.1", 81 | "eslint-plugin-jsx-a11y": "^6.8.0", 82 | "eslint-plugin-n": "^16.6.2", 83 | "eslint-plugin-prettier": "^5.1.3", 84 | "eslint-plugin-promise": "^6.1.1", 85 | "eslint-plugin-react": "^7.33.2", 86 | "eslint-plugin-react-hooks": "^4.6.0", 87 | "husky": "^9.0.10", 88 | "postcss": "^8", 89 | "prettier": "^3.2.5", 90 | "prettier-plugin-classnames": "^0.5.2", 91 | "typescript": "^5.3.3" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /client/public/approved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/approved.png -------------------------------------------------------------------------------- /client/public/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/avatar.png -------------------------------------------------------------------------------- /client/public/cloudflare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/cloudflare.png -------------------------------------------------------------------------------- /client/public/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/docker.png -------------------------------------------------------------------------------- /client/public/ecr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/ecr.png -------------------------------------------------------------------------------- /client/public/ecs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/ecs.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/hero1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/hero1.png -------------------------------------------------------------------------------- /client/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/nodejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/nodejs.png -------------------------------------------------------------------------------- /client/public/s3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/s3.png -------------------------------------------------------------------------------- /client/public/sqs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/sqs.png -------------------------------------------------------------------------------- /client/public/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/star.png -------------------------------------------------------------------------------- /client/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/verified-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amankumarsingh77/StreamScale-archived/54d71af0ccd3b91a13a7b01840e59cd149633ee3/client/public/verified-badge.png -------------------------------------------------------------------------------- /client/public/x-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/api/apiInstance.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const instance = axios.create({ 4 | withCredentials: true 5 | }) 6 | -------------------------------------------------------------------------------- /client/src/app/(user)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { instance } from '@/api/apiInstance' 4 | import ProtectedRoute from '@/components/ProtectedRoute' 5 | import { 6 | DataTableDemo, 7 | FileProps 8 | } from '@/components/dashboard/DemoListing' 9 | import DragAndDropUpload from '@/components/dashboard/DragAndDrop' 10 | import Typography from '@/components/ui/typography' 11 | import React, { useEffect, useState } from 'react' 12 | 13 | export default function Page() { 14 | const [files, setFiles] = useState([]) 15 | 16 | useEffect(() => { 17 | const fetchData = async () => { 18 | const response = await instance.get( 19 | `${process.env.NEXT_PUBLIC_API_URL}/api/file/getfiles` 20 | ) 21 | if (response.status === 200) { 22 | setFiles(response.data.files) 23 | } 24 | } 25 | fetchData() 26 | }, [setFiles]) 27 | return ( 28 |
29 | 30 | 31 | Dashboard 32 | 33 |
34 | 35 | 39 |
40 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /client/src/app/(user)/login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useEffect, useState } from 'react' 4 | import { zodResolver } from '@hookform/resolvers/zod' 5 | import { useForm } from 'react-hook-form' 6 | import { z } from 'zod' 7 | import { Button } from '@/components/ui/button' 8 | import { 9 | Form, 10 | FormControl, 11 | FormField, 12 | FormItem, 13 | FormLabel, 14 | FormMessage 15 | } from '@/components/ui/form' 16 | import { Input } from '@/components/ui/input' 17 | import Image from 'next/image' 18 | import Typography from '@/components/ui/typography' 19 | import { SignInItemLists } from '@/constants/authitems' 20 | import { useRouter } from 'next/navigation' 21 | import { useAuth } from '@/context/AuthContext' 22 | import { instance } from '@/api/apiInstance' 23 | 24 | interface ApiResponse { 25 | message: string 26 | status: number 27 | } 28 | 29 | const FormSchema = z.object({ 30 | username: z 31 | .string() 32 | .min(2, { 33 | message: 'Username must be at least 2 characters.' 34 | }) 35 | .max(18, { 36 | message: 'Username must be at most 18 characters.' 37 | }), 38 | password: z 39 | .string() 40 | .min(8, { 41 | message: 'Password must be at least 8 characters.' 42 | }) 43 | .trim() 44 | }) 45 | 46 | export default function Page() { 47 | const router = useRouter() 48 | const { setUser, user } = useAuth() 49 | useEffect(() => { 50 | if (user) { 51 | router.push('/dashboard') 52 | } 53 | }, [user]) 54 | const [error, setError] = useState('') 55 | const [loading, setLoading] = useState(false) 56 | 57 | const form = useForm>({ 58 | resolver: zodResolver(FormSchema), 59 | defaultValues: { 60 | username: '', 61 | password: '' 62 | } 63 | }) 64 | 65 | const onSubmit = async ( 66 | formData: z.infer 67 | ) => { 68 | setLoading(true) 69 | try { 70 | const response = await instance.post( 71 | `${process.env.NEXT_PUBLIC_API_URL}/api/user/login`, 72 | formData 73 | ) 74 | if (response.status === 200) { 75 | setUser(response.data) 76 | router.push('/dashboard') 77 | } else { 78 | setError(response.data.message) 79 | } 80 | } catch (error: any) { 81 | if (error.response) { 82 | setError(error.response.data.message) 83 | } else if (error.request) { 84 | setError('No response received from server') 85 | } else { 86 | setError('An error occurred: ' + error.message) 87 | } 88 | } finally { 89 | setLoading(false) 90 | } 91 | } 92 | 93 | return ( 94 |
98 |
99 | 103 | {SignInItemLists.map((item, index) => ( 104 | ( 109 | 110 | {item.name} 111 | 112 | 121 | 122 | 123 | 124 | )} 125 | /> 126 | ))} 127 | 134 | {error && ( 135 |
136 | {error} 137 |
138 | )} 139 | 140 | or 141 | 142 | 156 | 157 | 158 |
159 | ) 160 | } 161 | -------------------------------------------------------------------------------- /client/src/app/(user)/player/page.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import VideoPlayer from '@/components/player/VideoPlayer' 3 | 4 | const Page: React.FC = () => { 5 | return ( 6 | Loading...}> 7 | 8 | 9 | ) 10 | } 11 | 12 | export default Page 13 | -------------------------------------------------------------------------------- /client/src/app/(user)/profile/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useEffect, useState } from 'react' 4 | import { Avatar, AvatarImage } from '@/components/ui/avatar' 5 | import { Button } from '@/components/ui/button' 6 | import { Input } from '@/components/ui/input' 7 | import { Label } from '@/components/ui/label' 8 | import Typography from '@/components/ui/typography' 9 | import { Pencil1Icon } from '@radix-ui/react-icons' 10 | import { Tooltip } from '@nextui-org/tooltip' 11 | import { VerifiedIcon } from 'lucide-react' 12 | import { fetchUser } from '@/services/authService' 13 | import ProtectedRoute from '@/components/ProtectedRoute' 14 | import { Textarea } from '@/components/ui/textarea' 15 | import { instance } from '@/api/apiInstance' 16 | import { toast } from '@/components/ui/use-toast' 17 | 18 | interface UserProps { 19 | _id: string 20 | username: string 21 | email: string 22 | picture: string 23 | isAllowed: string 24 | fullname: string 25 | message: string 26 | } 27 | 28 | export default function Page() { 29 | const [userData, setUserData] = 30 | useState(null) 31 | const [initialData, setInitialData] = 32 | useState(null) 33 | const [error, setError] = useState(null) 34 | 35 | useEffect(() => { 36 | const fetchUserProfile = async () => { 37 | try { 38 | const data = await fetchUser() 39 | setUserData(data.user) 40 | setInitialData(data.user) 41 | } catch (error) { 42 | setUserData(null) 43 | } 44 | } 45 | fetchUserProfile() 46 | }, []) 47 | 48 | const handleInputChange = ( 49 | e: React.ChangeEvent< 50 | HTMLInputElement | HTMLTextAreaElement 51 | > 52 | ) => { 53 | const { name, value } = e.target 54 | setUserData((prevState) => { 55 | if (!prevState) return prevState 56 | return { 57 | ...prevState, 58 | [name]: value 59 | } 60 | }) 61 | } 62 | 63 | const updateHandler = async () => { 64 | setError(null) 65 | if (!userData || !initialData) return 66 | 67 | const updatedFields: Partial = {} 68 | for (const key in userData) { 69 | if ( 70 | userData[key as keyof UserProps] !== 71 | initialData[key as keyof UserProps] 72 | ) { 73 | updatedFields[key as keyof UserProps] = 74 | userData[key as keyof UserProps] 75 | } 76 | } 77 | 78 | console.log(updatedFields) 79 | 80 | try { 81 | await instance.patch( 82 | `${process.env.NEXT_PUBLIC_API_URL}/api/user/update`, 83 | updatedFields 84 | ) 85 | toast({ 86 | title: 'Success', 87 | description: 'Profile updated successfully' 88 | }) 89 | setInitialData(userData) 90 | } catch (error: any) { 91 | setError(error.response.data.message) 92 | } 93 | } 94 | 95 | return ( 96 | 97 |
98 |
99 | Profile 100 | 101 | Manage your profile and subscriptions 102 | 103 |
104 |
105 |
106 |
107 | 108 | User Profile 109 | 110 | 111 | Edit or view your profile 112 | 113 |
114 |
115 | 116 | 117 | 118 | {userData?.isAllowed && ( 119 | 125 | 126 | 127 | )} 128 | 129 | 130 |
131 |
132 |
133 | 134 | 141 |
142 |
143 | 144 | 151 |
152 |
153 | 154 | 161 |
162 | 163 | 168 | {!userData?.isAllowed && ( 169 | 170 | Your account is not verified. You will not 171 | be able to upload videos. 172 | 173 | )} 174 | {error && ( 175 | 176 | {error} 177 | 178 | )} 179 | 185 |
186 |
187 |
188 |
189 | Subscription 190 | Manage your subscription 191 |
192 | 193 | Coming soon.... 194 | 195 |
196 |
197 |
198 |
199 | ) 200 | } 201 | -------------------------------------------------------------------------------- /client/src/app/(user)/signup/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useEffect } from 'react' 4 | import { zodResolver } from '@hookform/resolvers/zod' 5 | import { useForm } from 'react-hook-form' 6 | import { z } from 'zod' 7 | import { Button } from '@/components/ui/button' 8 | import { 9 | Form, 10 | FormControl, 11 | FormField, 12 | FormItem, 13 | FormLabel, 14 | FormMessage 15 | } from '@/components/ui/form' 16 | import { Input } from '@/components/ui/input' 17 | import Image from 'next/image' 18 | import Typography from '@/components/ui/typography' 19 | import { SignUpItemLists } from '@/constants/authitems' 20 | import { Textarea } from '@/components/ui/textarea' 21 | import { useRouter } from 'next/navigation' 22 | import { instance } from '@/api/apiInstance' 23 | import { useAuth } from '@/context/AuthContext' 24 | 25 | const FormSchema = z.object({ 26 | username: z 27 | .string() 28 | .min(2, { 29 | message: 'Username must be at least 2 characters.' 30 | }) 31 | .max(18, { 32 | message: 'Username must be at most 18 characters.' 33 | }), 34 | email: z.string().email({ 35 | message: 'Invalid email address.' 36 | }), 37 | password: z 38 | .string() 39 | .min(8, { 40 | message: 'Password must be at least 8 characters.' 41 | }) 42 | .trim(), 43 | message: z 44 | .string() 45 | .min(10, { 46 | message: 'Message must be at least 10 characters.' 47 | }) 48 | .max(100, { 49 | message: 'Message must be at most 100 characters.' 50 | }), 51 | fullname: z 52 | .string() 53 | .min(2, { message: 'Minimum 2 characters required' }) 54 | }) 55 | 56 | export default function Page() { 57 | const router = useRouter() 58 | const [error, setError] = React.useState('') 59 | const [loading, setLoading] = React.useState(false) 60 | const form = useForm>({ 61 | resolver: zodResolver(FormSchema), 62 | defaultValues: { 63 | username: '', 64 | email: '', 65 | password: '', 66 | message: '', 67 | fullname: '' 68 | } 69 | }) 70 | const { user } = useAuth() 71 | useEffect(() => { 72 | if (user) { 73 | router.push('/dashboard') 74 | } 75 | }, [user]) 76 | 77 | const onSubmit = async ( 78 | data: z.infer 79 | ) => { 80 | setLoading(true) 81 | const response = await instance.post( 82 | `${process.env.NEXT_PUBLIC_API_URL}/api/user/register`, 83 | data 84 | ) 85 | setLoading(false) 86 | if (response.status === 200) { 87 | router.push('/login') 88 | } else { 89 | setError(response.data.message) 90 | } 91 | } 92 | 93 | return ( 94 |
98 |
99 | 103 | {SignUpItemLists.map((item, index) => ( 104 | ( 109 | 110 | {item.name} 111 | 112 | 121 | 122 | 123 | 124 | )} 125 | /> 126 | ))} 127 | ( 131 | 132 | 133 | Message 134 | 135 | 136 |