From 777de35c6541208eb811570a2db6c8f4b9df849e Mon Sep 17 00:00:00 2001 From: mihir_dml Date: Tue, 11 Feb 2025 11:23:59 +0530 Subject: [PATCH] comment added --- sequelize-cli.js | 6 + server/app.js | 39 + server/bin/index.js | 77 + server/config/config.json | 9 + server/config/privateKeys.js | 22 + server/controller/assessment/index.js | 348 +++++ server/controller/users/index.js | 1389 +++++++++++++++++ server/helper/catchAsyncAction/index.js | 8 + server/helper/index.js | 5 + server/helper/jwt/index.js | 59 + server/helper/makeResponse/index.js | 51 + server/helper/qrcode_generator/index.js | 12 + server/helper/s3/index.js | 35 + server/loaders/db/index.js | 23 + server/loaders/index.js | 1 + server/middlewares/auth.js | 39 + server/middlewares/upload.js | 70 + ...1212070757-add-metrics-to-user-uploads.cjs | 123 ++ server/model/assessment.js | 40 + server/model/training.js | 56 + server/model/user.js | 70 + server/model/userAnswered.js | 48 + server/model/userUpload.js | 99 ++ server/models/index.js | 43 + server/routes/assessment.js | 14 + server/routes/index.js | 15 + server/routes/users/user.js | 58 + server/services/assessment.js | 94 ++ server/services/users.js | 156 ++ 29 files changed, 3009 insertions(+) create mode 100644 sequelize-cli.js create mode 100644 server/app.js create mode 100644 server/bin/index.js create mode 100644 server/config/config.json create mode 100755 server/config/privateKeys.js create mode 100644 server/controller/assessment/index.js create mode 100644 server/controller/users/index.js create mode 100755 server/helper/catchAsyncAction/index.js create mode 100755 server/helper/index.js create mode 100644 server/helper/jwt/index.js create mode 100755 server/helper/makeResponse/index.js create mode 100755 server/helper/qrcode_generator/index.js create mode 100755 server/helper/s3/index.js create mode 100755 server/loaders/db/index.js create mode 100755 server/loaders/index.js create mode 100644 server/middlewares/auth.js create mode 100644 server/middlewares/upload.js create mode 100644 server/migrations/20241212070757-add-metrics-to-user-uploads.cjs create mode 100644 server/model/assessment.js create mode 100644 server/model/training.js create mode 100644 server/model/user.js create mode 100644 server/model/userAnswered.js create mode 100644 server/model/userUpload.js create mode 100644 server/models/index.js create mode 100644 server/routes/assessment.js create mode 100644 server/routes/index.js create mode 100644 server/routes/users/user.js create mode 100644 server/services/assessment.js create mode 100644 server/services/users.js diff --git a/sequelize-cli.js b/sequelize-cli.js new file mode 100644 index 0000000..283707a --- /dev/null +++ b/sequelize-cli.js @@ -0,0 +1,6 @@ +// sequelize-cli.js +require('module-alias/register'); +// const { config } = require('./config/config.json'); // Import the configuration file + +import { sequelize } from './server/loaders'; +module.exports = sequelize; diff --git a/server/app.js b/server/app.js new file mode 100644 index 0000000..f1526ab --- /dev/null +++ b/server/app.js @@ -0,0 +1,39 @@ +import express from 'express'; +import { createServer } from 'http'; +import cookieParser from 'cookie-parser'; +import logger from 'morgan'; +import indexRouter from './routes/index.js'; +import { renderFile } from 'ejs'; +import dotenv from "dotenv"; +import cors from "cors"; +dotenv.config(); +import './loaders/index.js'; +import { makeResponse } from './helper/index.js'; + +const app = express(); +const server = createServer(app); + +app.use(logger('dev')); +app.use(express.json()); +// enable cors +app.use(cors()); +app.options('*', cors()); +app.use('/public/', express.static('public')); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.engine('html', renderFile); +app.set('view engine', 'html'); +app.use('/', indexRouter); +// cron job +// Custom error-handling middleware +app.use((err, req, res, next) => { + if (err) { + const statusCode = err.customCode || 500; + const message = err.message || 'Internal Server Error'; + return makeResponse(res, statusCode, false, message) + } else { + next(); + } +}); + +export default app; diff --git a/server/bin/index.js b/server/bin/index.js new file mode 100644 index 0000000..ac26872 --- /dev/null +++ b/server/bin/index.js @@ -0,0 +1,77 @@ +import app from "../app.js"; +import debugLib from "debug"; +import http from "http"; +const debug = debugLib("your-project-name:server"); +import dotenv from "dotenv"; +import { Server } from "socket.io"; +dotenv.config(); + +/** + * Get port from environment and store in Express. + */ + +const port = process.env.PORT || "3001"; +console.log("App is running on port", port); +app.set("port", port); + +/** + * Create HTTP server. + */ + +const server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on("error", onError); +server.on("listening", onListening); + +const io = new Server(server, { + maxHttpBufferSize: 1e9, + //transports: ['websocket'], upgrade: false, + pingInterval: 1000 * 60 * 5, + pingTimeout: 1000 * 60 * 3, + cors: { + origin: "*", + }, +}); + +/** + * Event listener for HTTP server "error" event. + * @param {Object} error - The error object containing details about the error. + * @throws {Error} - Rethrows the error if it's not a listening error. + */ + +function onError(error) { + if (error.syscall !== "listen") { + throw error; + } + + let bind = typeof port === "string" ? "Pipe " + port : "Port " + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case "EACCES": + console.error(bind + " requires elevated privileges"); + process.exit(1); + break; + case "EADDRINUSE": + console.error(bind + " is already in use"); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + const addr = server.address(); + const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; + debug("Listening on " + bind); +} diff --git a/server/config/config.json b/server/config/config.json new file mode 100644 index 0000000..5124e7c --- /dev/null +++ b/server/config/config.json @@ -0,0 +1,9 @@ +{ + "development": { + "username": "tennish", + "password": "Tensskdb@124", + "database": "Ai-Tennis-coach", + "host": "14.97.60.131", + "dialect": "postgres" + } +} diff --git a/server/config/privateKeys.js b/server/config/privateKeys.js new file mode 100755 index 0000000..ba6c9b9 --- /dev/null +++ b/server/config/privateKeys.js @@ -0,0 +1,22 @@ +import dotenv from "dotenv"; +dotenv.config(); + +let { + NODE_ENV, + PORT, + DB_STRING_DEV, + TOKEN_SECRET, + REFRESH_TOKEN_SECRET, + NODE_SERVER_URL, + AI_SERVER_URL +} = process.env; + +export const privateKey = { + 'NODE_ENV':NODE_ENV, + 'PORT':PORT, + 'DB_STRING_DEV':DB_STRING_DEV, + 'TOKEN_SECRET':TOKEN_SECRET, + 'REFRESH_TOKEN_SECRET':REFRESH_TOKEN_SECRET, + 'NODE_SERVER_URL':NODE_SERVER_URL, + 'AI_SERVER_URL':AI_SERVER_URL +} diff --git a/server/controller/assessment/index.js b/server/controller/assessment/index.js new file mode 100644 index 0000000..6a90192 --- /dev/null +++ b/server/controller/assessment/index.js @@ -0,0 +1,348 @@ +import { catchAsyncAction, generateToken, makeResponse } from "../../helper/index.js"; +import { responseMessages, statusCodes } from '../../helper/makeResponse/index.js'; +import { addUserAnswersByUserId, addUserDrillsByUserId, createQuestions, getAllQuestions, getQuestionById, getUserAnswersByUserId, getUserTrainingByUserId, updateQuestionById } from "../../services/assessment.js"; +import { getUserById } from "../../services/users.js"; +import { privateKey } from "../../config/privateKeys.js"; +import axios from "axios"; +/** + * API handler to add assessment questions. + * Validates input and creates new questions in the database. + * + * @param {Object} req - The request object containing assessment questions. + * @param {Object} res - The response object used to send responses back to the client. + * @returns {Object} - A response object indicating the success or failure of the operation. + */ +export const addAssessmentQuestions = catchAsyncAction(async (req, res) => { + const questions = req.body.assessment; // Extracting assessment questions from the request body + + // Input validation: ensure the array is not empty + if (!Array.isArray(questions) || questions.length === 0) { + return makeResponse(res, statusCodes.BAD_REQUEST, false, 'Request body must be an array of questions.', {}); + } + + + for (const questionData of questions) { + const { question } = questionData; // Destructuring question from questionData + + // Validate that all necessary fields are present + if (!question) { + return makeResponse(res, statusCodes.BAD_REQUEST, false, 'All fields are required.', {}); + } + questionData.options = JSON.stringify(questionData.options || []); // Stringify options if they exist + } + + try { + const questionData = await createQuestions(questions); // Create questions in the database + + return makeResponse(res, statusCodes.SUCCESS, true, responseMessages.QUESTIONS_CREATED, questionData); + } catch (error) { + const code = error?.code || statusCodes.SERVER_ERROR; + const message = error?.message || responseMessages.SOMETHING_WRONG; + + return makeResponse(res, code, false, message, {}); + } +}); + +/** + * API handler to update an assessment question by its ID. + * Validates input and updates the question in the database. + * + * @param {Object} req - The request object containing the question ID and updated data. + * @param {Object} res - The response object used to send responses back to the client. + * @returns {Object} - A response object indicating the success or failure of the operation. + */ +export const updateAssessmentQuestionById = catchAsyncAction(async (req, res) => { + const { id } = req.params; // Get the question id from the URL + const { question, options, orderId } = req.body; // Get the updated data from the request body + + try { + // Find the question by ID + const foundQuestion = await getQuestionById(id); + + if (!foundQuestion) { + return makeResponse(res, statusCodes.NOT_FOUND, false, responseMessages.QUESTIONS_NOT_FOUND, {}); + } + + // Update the question with new data + foundQuestion.question = question || foundQuestion.question; // Update question if provided + foundQuestion.options = JSON.stringify(options) || foundQuestion.options; // Update options if provided + foundQuestion.orderId = orderId || foundQuestion.orderId; // Update orderId if provided + + await updateQuestionById(foundQuestion); // Save the updated question + + return makeResponse(res, statusCodes.SUCCESS, true, responseMessages.QUESTIONS_UPDATED, foundQuestion); // Return success response + + } catch (error) { + const code = error?.code || statusCodes.SERVER_ERROR; + const message = error?.message || responseMessages.SOMETHING_WRONG; + + return makeResponse(res, code, false, message, {}); + } +}); + +/** + * API handler to get all assessment questions. + * Fetches all questions from the database and returns them. + * + * @param {Object} req - The request object. + * @param {Object} res - The response object used to send responses back to the client. + * @returns {Object} - A response object containing the fetched questions or an error message. + */ +export const getAssessmentQuestions = catchAsyncAction(async (req, res) => { + try { + const questions = await getAllQuestions(); // Get all questions from DB + + if (!questions || questions.length === 0) { + return makeResponse(res, statusCodes.NOT_FOUND, false, responseMessages.QUESTIONS_NOT_FOUND, {}); + } + + // Return all questions in the response + return makeResponse(res, statusCodes.SUCCESS, true, responseMessages.QUESTIONS_FETCHED, questions); + + } catch (error) { + const code = error?.code || statusCodes.SERVER_ERROR; + const message = error?.message || responseMessages.SOMETHING_WRONG; + + return makeResponse(res, code, false, message, {}); + } +}); + +/** + * API handler to add user assessment responses. + * Validates user input and saves user responses to the database. + * + * @param {Object} req - The request object containing user responses. + * @param {Object} res - The response object used to send responses back to the client. + * @returns {Object} - A response object indicating the success or failure of the operation. + */ +export const addUserAssessmentResponse = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + + try { + // Fetch current user details to check for changes + const currentUser = await getUserById(userId); + + if (!currentUser) { + return makeResponse(res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {}); + } + + // Initialize reqBody with default empty values + let reqBody = { + experience: "", + frequency: "", + struggle_area: "", + playing_style: "", + improvement_focus: "", + session_duration: "" + }; + + + const answers = req.body.answers || []; // Extract answers from request body + answers.forEach((answer) => { + switch (answer.questionId) { + case 1: + reqBody.experience = answer.selectedOption; // Set experience based on selectedOption + break; + case 2: + reqBody.frequency = answer.selectedOption; // Set frequency based on selectedOption + break; + case 3: + reqBody.struggle_area = answer.selectedOption; // Set struggle_area based on selectedOption + break; + case 4: + reqBody.playing_style = answer.selectedOption; // Set playing_style based on selectedOption + break; + case 6: + reqBody.improvement_focus = answer.selectedOption; // Set improvement_focus based on selectedOption + break; + case 7: + reqBody.session_duration = answer.selectedOption; // Set session_duration based on selectedOption + break; + default: + break; // Ignore unrecognized question IDs + } + }); + + // Check if all keys in reqBody are non-empty + const missingFields = []; + for (const [key, value] of Object.entries(reqBody)) { + if (value === "" || value === null || value === undefined) { + missingFields.push(key); + } + } + + if (missingFields.length > 0) { + // If there are missing fields, return an error response + return makeResponse(res, statusCodes.BAD_REQUEST, false, "Please submit all answers", {}); + } + + const analysisData = await getTrainingAnalysis(reqBody); // Get training analysis based on user responses + if (!analysisData) { + return makeResponse(res, statusCodes.BAD_REQUEST, false, "Something went wrong", {}); + } + + const trainingData = { + drills: analysisData.data.drills, + recommendation: analysisData.data.recommendation, + videos: analysisData.data.videos, + yoga_exercises: analysisData.data.yoga_exercises, + weekly_plan: analysisData.data.weekly_plan, + videos_watched: analysisData.data.videos_watched ?? [], + } + trainingData.user_id = userId; + + + const addFields = { + user_id: userId, + answers: JSON.stringify(answers) || "" + }; + + const data = await addUserAnswersByUserId(addFields); // Save user answers + const trainingDrillsData = await addUserDrillsByUserId(trainingData); // Save training data + console.log(trainingDrillsData); // Log training drills data for debugging + + + return makeResponse(res, statusCodes.SUCCESS, true, responseMessages.DATA_ADDED, { answersRes: data, trainingDrillsData }); + + } catch (error) { + const code = error?.code || statusCodes.SERVER_ERROR; + const message = error?.message || responseMessages.SOMETHING_WRONG; + + return makeResponse(res, code, false, message, {}); + } +}); + +/** + * API handler to get user answers. + * Fetches user answers from the database and returns them. + * + * @param {Object} req - The request object. + * @param {Object} res - The response object used to send responses back to the client. + * @returns {Object} - A response object containing the fetched answers or an error message. + */ +export const getUserAnswers = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + try { + // Fetch current user details to check for changes + const currentUser = await getUserById(userId); + + if (!currentUser) { + return makeResponse(res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {}); + } + + const data = await getUserAnswersByUserId(userId); // Get user answers + + // Return the updated user data (excluding password) + return makeResponse(res, statusCodes.SUCCESS, true, responseMessages.DATA_FETCHED, data); + } catch (error) { + const code = error?.code || statusCodes.SERVER_ERROR; + const message = error?.message || responseMessages.SOMETHING_WRONG; + + return makeResponse(res, code, false, message, {}); + } +}); + +/** + * Helper function to handle AI analysis. + * Sends user data to the AI server for analysis and returns the response. + * + * @param {Object} body - The data to be sent to the AI server. + * @returns {Object|null} - The response from the AI server or null if an error occurs. + */ +const getTrainingAnalysis = async (body) => { + const postApiUrl = `${privateKey.AI_SERVER_URL}recommend`; // Construct the API URL + console.log(postApiUrl); // Log the API URL for debugging + try { + const aiResponse = await axios.post(postApiUrl, body); // Send POST request to AI server + return aiResponse; + } catch (err) { + console.error('AI analysis error:', err); + return null; + } +}; + +/** + * API handler to get user training details for a specific day. + * Fetches training data from the database and filters it by day. + * + * @param {Object} req - The request object containing the day query parameter. + * @param {Object} res - The response object used to send responses back to the client. + * @returns {Object} - A response object containing the training data for the specified day or an error message. + */ +export const getUserDayTraining = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + try { + const { day } = req.query; // Get day from query parameters + + const currentUser = await getUserById(userId); + + if (!currentUser) { + return makeResponse(res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {}); + } + + const data = await getUserTrainingByUserId(userId); + if (!data) { + return makeResponse(res, statusCodes.NOT_FOUND, false, 'Training Details Not Found', {}); + } + let dayObject = data; + if (day) { + dayObject = data?.dataValues?.weekly_plan.filter(item => item.day === `Day ${day}`)[0]; // Filter training data by day + dayObject.videos_watched = data?.dataValues?.videos_watched; + } + return makeResponse(res, statusCodes.SUCCESS, true, responseMessages.DATA_FETCHED, dayObject ?? 'No training for the day'); // Return training data or message + } catch (error) { + const code = error?.code || statusCodes.SERVER_ERROR; + const message = error?.message || responseMessages.SOMETHING_WRONG; + return makeResponse(res, code, false, message, {}); + } +}); + +/** + * API handler to update the list of watched videos by the user. + * Validates input and updates the training record in the database. + * + * @param {Object} req - The request object containing the day query parameter. + * @param {Object} res - The response object used to send responses back to the client. + * @returns {Object} - A response object indicating the success or failure of the operation. + */ +export const updateVideoWatchByUser = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + try { + const { day } = req.query; // Get day from query parameters + // Fetch current user details to check for changes + const currentUser = await getUserById(userId); + + if (!currentUser) { + return makeResponse(res, statusCodes.NOT_FOUND, false, responseMessages.USER_NOT_FOUND, {}); + } + + + const training = await getUserTrainingByUserId(userId); + + if (!training) { + return makeResponse(res, statusCodes.NOT_FOUND, false, 'Training Details Not Found', {}); + } + + // Ensure videos_watched is initialized as an empty array if it's null or undefined + if (!Array.isArray(training.videos_watched)) { + training.videos_watched = []; + } + + const newVideo = day; // Set new video to be added + + + if (!training.videos_watched.includes(newVideo)) { + // Add the new value to the array if it doesn't exist + training.videos_watched = [...training.videos_watched, newVideo]; // Update the watched videos array + } + + // Save the updated training record + const data = await training.save(); // Persist changes to the database + + return makeResponse(res, statusCodes.SUCCESS, true, responseMessages.DATA_FETCHED, data); + } catch (error) { + const code = error?.code || statusCodes.SERVER_ERROR; + const message = error?.message || responseMessages.SOMETHING_WRONG; + return makeResponse(res, code, false, message, {}); + } +}); diff --git a/server/controller/users/index.js b/server/controller/users/index.js new file mode 100644 index 0000000..f672fc8 --- /dev/null +++ b/server/controller/users/index.js @@ -0,0 +1,1389 @@ +import { + catchAsyncAction, + generateToken, + makeResponse, +} from '../../helper/index.js'; +import { + responseMessages, + statusCodes, +} from '../../helper/makeResponse/index.js'; +import { + countUsers, + countUserUploadsByUserId, + createUpload, + createUser, + getUploadVideoById, + getUserByEmail, + getUserByFilter, + getUserById, + getUserUpload, + getUserUploadByDateFilter, + updateUserProfile, +} from '../../services/users.js'; +import bcrypt from 'bcrypt'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; +import { privateKey } from '../../config/privateKeys.js'; +import axios from 'axios'; +import upload from '../../middlewares/upload.js'; +import moment from 'moment'; +import { Op } from 'sequelize'; +import { getUserAnswersByUserId } from '../../services/assessment.js'; + +// Get the current module directory +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * Handles user login by validating credentials and generating a JWT token. + * + * @param {Object} req - The request object containing user credentials. + * @param {Object} res - The response object used to send responses. + * @throws {Error} - If an error occurs during the login process. + * @returns {Object} - The response object with user information and tokens. + */ +export const userLogin = catchAsyncAction(async (req, res) => { + const { email, password, rememberMe } = req.body; + + // Validate required fields + if (!email || !password) { + return makeResponse( + res, + statusCodes.BAD_REQUEST, + false, + 'Email and password are required', + {} + ); + } + + try { + // Check if the user exists + const user = await getUserByEmail(email); + if (!user) { + return makeResponse( + res, + statusCodes.BAD_REQUEST, + false, + responseMessages.USER_NOT_FOUND, + {} + ); + } + const updatedUser = await updateUserProfile(user.id, { rememberMe }); + + // Compare the provided password with the hashed password in the database + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + return makeResponse( + res, + statusCodes.UNAUTHORIZED, + false, + responseMessages.INVALID_CREDENTIALS, + {} + ); + } + + const QuizPlayed = await getUserAnswersByUserId(user.id); + + // If login is successful, generate a JWT token + const payload = { + userId: user.id, + email: user.email, + username: user.username, + fullname: user.full_name, + }; + + // Generate a JWT token using the payload and rememberMe flag + const token = await generateToken(payload, rememberMe); // JWT secret stored in .env + + // Prepare the response data including user information and tokens + const responseData = { + user: { + id: user.id, + username: user.username, + email: user.email, + phoneNumber: user.phone_number, + isQuizPlayed: QuizPlayed.length > 0 ? true : false, // Check if the user has played a quiz + profilePic: user.profile_pic ? privateKey.NODE_SERVER_URL + user?.profile_pic : "", // User profile picture URL + rememberMe + }, + token: token.accessToken, // Access token for authentication + refreshToken: token.refreshToken // Refresh token for obtaining new access tokens + }; + + // Send the response with a success status and the response data + return makeResponse( + res, + statusCodes.SUCCESS, + true, + responseMessages.LOGIN_SUCCESS, + responseData + ); + } catch (error) { + // Handle errors that may occur during the process + const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code + const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message + + // Send an error response + return makeResponse(res, code, false, message, {}); + } +}); + + +/** + * Handles user signup by validating input, checking for existing users, + * hashing the password, and creating a new user record in the database. + * + * @param {Object} req - The request object containing user signup data. + * @param {Object} res - The response object used to send responses back to the client. + * @returns {Object} - A response object indicating the success or failure of the signup process. + * @throws {Error} - If an error occurs during the signup process, an appropriate error response is returned. + */ +export const userSignUp = catchAsyncAction(async (req, res) => { + // Destructure user details from the request body + const { username, email, password, phoneNumber, fullName } = req.body; + + // Check if the environment is development and limit the number of signups + if (privateKey.NODE_ENV === 'development') { + const usercount = await countUsers(); + // If the user count exceeds 30, return an error response + if (usercount >= 30) { + return makeResponse( + res, + statusCodes.BAD_REQUEST, + false, + 'Max 8 signup limit reached', + {} + ); + } + } + + // Validate required fields and return an error if any are missing + if (!username || !email || !password || !phoneNumber || !fullName) { + return makeResponse( + res, + statusCodes.BAD_REQUEST, + false, + 'Missing required fields', + {} + ); + } + + try { + // Check if the email already exists in the database + const emailCheckResult = await getUserByEmail(email); + + // If the email is found, return an error response + if (emailCheckResult) { + return makeResponse( + res, + statusCodes.BAD_REQUEST, + false, + responseMessages.USER_ALREADY_EXISTS, + {} + ); + } + + // Check if the username already exists + const usernameExists = await getUserByFilter({ username }); + // If the username is found, return an error response + if (usernameExists) { + return makeResponse( + res, + statusCodes.BAD_REQUEST, + false, + responseMessages.USERNAME_ALREADY_EXISTS, + {} + ); + } + + // Hash the password using bcrypt with a specified number of salt rounds + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(password, saltRounds); + + // Create a new user record in the database + const result = await createUser({ + username, + email, + password: hashedPassword, + phone_number: phoneNumber, + full_name: fullName + }); + + // Send back the newly created user data, excluding the password + const user = result.dataValues; + // Delete the password field from the user object + delete user.password; + return makeResponse( + res, + statusCodes.SUCCESS, + true, + responseMessages.USER_CREATED, + user + ); + } catch (error) { + const code = error?.code || statusCodes.SERVER_ERROR; + console.log(error); + const message = error?.message || responseMessages.SOMETHING_WRONG; + + return makeResponse(res, code, false, message, {}); + } +}); + +/** + * Handles the user profile update process by validating input, + * checking for existing usernames and emails, and updating user details + * in the database. It also manages password hashing and profile picture uploads. + * + * @param {Object} req - The request object containing user profile data. + * @param {Object} res - The response object used to send responses back to the client. + * @returns {Object} - A response object indicating the success or failure of the update process. + * @throws {Error} - If an error occurs during the update process, an appropriate error response is returned. + */ +export const updateUserProfileHandler = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + const { username, fullName, phoneNumber, password, email, age, gender, location } = req.body; + + try { + // Fetch current user details to check for changes + const currentUser = await getUserById(userId); + + // If the user is not found, return a NOT_FOUND response + if (!currentUser) { + return makeResponse( + res, + statusCodes.NOT_FOUND, + false, + responseMessages.USER_NOT_FOUND, + {} + ); + } + + // Prepare fields to be updated, and only update those fields that are provided in the request + const updatedFields = {}; + + // Check if a new username is provided and validate its uniqueness + if (username) { + const usernameExists = await getUserByFilter({ username, id: { [Op.ne]: userId } }); // Ensures id is not equal to userId + if (usernameExists) { + return makeResponse( + res, + statusCodes.BAD_REQUEST, + false, + responseMessages.USERNAME_ALREADY_EXISTS, + {} + ); + } + updatedFields.username = username; // Add username to updatedFields + } + + // Check if a new email is provided and validate its uniqueness + if (email) { + const userExists = await getUserByFilter({ email, id: { [Op.ne]: userId } }); + if (userExists) { + return makeResponse( + res, + statusCodes.BAD_REQUEST, + false, + responseMessages.USER_ALREADY_EXISTS, + {} + ); + } + updatedFields.email = email; // Add email to updatedFields + } + + // Update other fields if provided + if (phoneNumber) updatedFields.phone_number = phoneNumber; + if (fullName) updatedFields.full_name = fullName; + if (age) updatedFields.age = age; + if (gender) updatedFields.gender = gender.toUpperCase(); + if (location) updatedFields.location = location.toUpperCase(); + + // If a new password is provided, hash it and add it to the updatedFields object + if (password) { + const saltRounds = 10; // Define salt rounds for hashing + updatedFields.password = await bcrypt.hash(password, saltRounds); // Hash the password + } + + // If a new profile picture is uploaded, update the profile_pic field + if (req.files && req.files.profilePic) { + const profilePic = req.files.profilePic[0]; // Access the uploaded file + const profilePicPath = path.join('public', 'images', profilePic.filename); // Path to save in DB + + // Delete the old profile picture if it exists + if (currentUser.profile_pic) { + // Extract the filename from the database value (assuming profile_pic has the full path in DB) + const oldProfilePicFilename = path.basename(currentUser.profile_pic); + + // Construct the full path to the old profile pic + const oldProfilePicPath = path.join( + __dirname, + '../../..', + 'public', + 'images', + oldProfilePicFilename + ); + + // Debugging: Print the full path being checked + console.log('Attempting to delete file at path:', oldProfilePicPath); + + // Check if the old profile pic exists before attempting to delete + fs.exists(oldProfilePicPath, (exists) => { + if (exists) { + console.log('Old profile picture exists, attempting to delete...'); + fs.unlink(oldProfilePicPath, (err) => { + if (err) { + console.log('Error deleting old profile pic', err); + } else { + console.log('Old profile picture deleted successfully'); + } + }); + } else { + console.log( + 'Old profile picture does not exist at path:', + oldProfilePicPath + ); + } + }); + } + + updatedFields.profile_pic = profilePicPath; // Save the new profile picture path in the DB + } + + // If no fields to update, return a message + if (Object.keys(updatedFields).length === 0) { + return makeResponse( + res, + statusCodes.BAD_REQUEST, + false, + 'No fields to update', // Message indicating no updates + {} + ); + } + + // Update user profile with only the provided fields + const updatedUser = await updateUserProfile(userId, updatedFields); + + // Ensure the password is not included in the response + delete updatedUser.dataValues.password; + + // Construct the full URL for the profile picture if it exists + updatedUser.profile_pic = updatedUser?.dataValues?.profile_pic ? privateKey.NODE_SERVER_URL + updatedUser?.dataValues?.profile_pic : ""; + + // Return the updated user data (excluding password) + return makeResponse( + res, + statusCodes.SUCCESS, + true, + responseMessages.USER_UPDATED, + updatedUser // Return updated user data + ); + } catch (error) { + // Handle any errors that occur during the process + const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code + const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message + + return makeResponse(res, code, false, message, {}); // Return error response + } +}); + + + +// Helper function to handle AI analysis +const getAIAnalysis = async (videoFilePath, type) => { + const postApiUrl = `${privateKey.AI_SERVER_URL}upload?video_url=${privateKey.NODE_SERVER_URL}${videoFilePath}&type=${type}`; + console.log(postApiUrl); + try { + const aiUploadResponse = await axios.post(postApiUrl); + + if (!aiUploadResponse.data.analysis_id) return null; + + const analysisData = { + overall_metrics: JSON.stringify(aiUploadResponse.data), + total_duration: aiUploadResponse.data.analysis_json.total_duration, + shots: {}, + }; + + // Fetch shot-specific analysis + for (const shot of aiUploadResponse.data.analysis_json.shots) { + analysisData.shots[shot.classification] = JSON.stringify(shot); + } + + return analysisData; + } catch (err) { + console.error('AI analysis error:', err); + return null; + } +}; + +const getAIManualAnalysis = async (data) => { + // Construct the API URL for manual analysis + const postApiUrl = `${privateKey.AI_SERVER_URL}manual_analysis`; + console.log(postApiUrl); + try { + // Send a POST request to the AI server with the provided data + const aiUploadResponse = await axios.post(postApiUrl, data); + + // Check if the response contains an analysis ID + if (!aiUploadResponse.data.analysis_id) return null; + + // Prepare the analysis data structure + const analysisData = { + overall_metrics: JSON.stringify(aiUploadResponse.data), // Store overall metrics as a JSON string + total_duration: aiUploadResponse.data.analysis_json.total_duration, // Extract total duration + shots: {}, // Initialize shots object + }; + + // Fetch shot-specific analysis + for (const shot of aiUploadResponse.data.analysis_json.shots) { + analysisData.shots[shot.classification] = JSON.stringify(shot); // Store each shot's analysis + } + + return analysisData; // Return the prepared analysis data + } catch (err) { + console.error('AI manual analysis error:', err); // Log any errors that occur + return null; // Return null in case of an error + } +}; + +/** + * Handles the upload of a user's recorded tennis video, processes AI analysis on the video, + * and updates the user's video record in the database. + * + * @param {Object} req - The request object containing user video upload data. + * @param {Object} res - The response object used to send responses back to the client. + * @returns {Object} - A response object indicating the success or failure of the video upload process. + * @throws {Error} - If an error occurs during the video upload or processing, an appropriate error response is returned. + */ +export const updateUserRecorderVideo = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + + try { + // Fetch the current user details based on userId + const currentUser = await getUserById(userId); + + // If the user is not found, return a NOT_FOUND response + if (!currentUser) { + return makeResponse( + res, + statusCodes.NOT_FOUND, + false, + responseMessages.USER_NOT_FOUND, + {} + ); + } + + // Get today's date using moment.js + const today = moment(); + + // Get the day of the week + const dayOfWeek = today.format('dddd'); + + // Prepare fields to be updated in the database + const updatedFields = { + user_id: userId, + file_name: '', + file_path: '', + file_type: '', + upload_day: dayOfWeek, + overall_metrics: '', + backhand: '', + forehand: '', + service: '', + smash: '', + total_duration: '', + }; + + // Handle file upload (video) + if (req.files && req.files.userVideo) { + const uploadedFile = req.files.userVideo[0]; // Access the uploaded video file + const uploadedFilepath = path.join( + 'public', + 'videos', + uploadedFile.filename // Path to save the uploaded video + ); + + // Update fields with the uploaded file details + updatedFields.file_name = uploadedFile.filename; + updatedFields.file_path = uploadedFilepath; + updatedFields.file_type = uploadedFile.mimetype; + } + + // If a video file is uploaded, process AI analysis + if (updatedFields.file_path) { + const aiAnalysisData = await getAIAnalysis(updatedFields.file_path, 1); // Perform AI analysis on the video + + // If AI analysis data is returned, update the relevant fields + if (aiAnalysisData) { + updatedFields.overall_metrics = aiAnalysisData.overall_metrics; + updatedFields.total_duration = aiAnalysisData.total_duration; + Object.assign(updatedFields, aiAnalysisData.shots); // Merge shot data into updatedFields + } + } + + console.log(updatedFields); // Debugging: Log the updated fields + + // Validate that overall metrics have been provided + if (updatedFields?.overall_metrics === '') { + const code = statusCodes.BAD_REQUEST; + const message = "Please upload the tennis specific video."; // Message indicating the need for a valid video + return makeResponse(res, code, false, message, {}); + } + + // Save the updated data in the database + const data = await createUpload(updatedFields); + + // Prepare the response data to return to the client + const updatedData = { + id: data.id, + user_id: data.user_id, + file_path: data.file_path + ? privateKey.NODE_SERVER_URL + data.file_path // Construct full URL for the file path + : '', + total_duration: data.total_duration, + upload_time: data.upload_time, + upload_day: data.upload_day, + }; + + // Return a success response with the updated data + return makeResponse( + res, + statusCodes.SUCCESS, + true, + 'Video uploaded successfully', + updatedData + ); + } catch (error) { + + const code = error?.code || statusCodes.SERVER_ERROR; + const message = error?.message || responseMessages.SOMETHING_WRONG; + return makeResponse(res, code, false, message, {}); + } +}); + + +export const updateUserRecorderMiniVideo = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + + try { + const currentUser = await getUserById(userId); + + if (!currentUser) { + return makeResponse( + res, + statusCodes.NOT_FOUND, + false, + responseMessages.USER_NOT_FOUND, + {} + ); + } + + // Get today's date in the format dd-mm-yyyy + const today = moment(); + + + const dayOfWeek = today.format('dddd'); + + const updatedFields = { + user_id: userId, + file_name: '', + file_path: '', + file_type: '', + upload_day: dayOfWeek, + overall_metrics: '', + backhand: '', + forehand: '', + service: '', + smash: '', + total_duration: '', + }; + + // Handle file upload (video) + if (req.files && req.files.userVideo) { + const uploadedFile = req.files.userVideo[0]; + const uploadedFilepath = path.join( + 'public', + 'videos', + uploadedFile.filename + ); + + updatedFields.file_name = uploadedFile.filename; + updatedFields.file_path = uploadedFilepath; + updatedFields.file_type = uploadedFile.mimetype; + } + + // If video file is uploaded, process AI analysis + if (updatedFields.file_path) { + const aiAnalysisData = await getAIAnalysis(updatedFields.file_path, 0); + + if (aiAnalysisData) { + updatedFields.overall_metrics = aiAnalysisData.overall_metrics; + updatedFields.total_duration = aiAnalysisData.total_duration; + Object.assign(updatedFields, aiAnalysisData.shots); + } + } + + if (updatedFields?.overall_metrics === '') { + const code = statusCodes.BAD_REQUEST; + const message = "Please upload the tennis specific video."; + return makeResponse(res, code, false, message, {}); + } + // Save the updated data + const data = await createUpload(updatedFields); + + const updatedData = { + id: data.id, + user_id: data.user_id, + file_path: data.file_path + ? privateKey.NODE_SERVER_URL + data.file_path + : '', + total_duration: data.total_duration, + upload_time: data.upload_time, + upload_day: data.upload_day, + overall_metrics: data.overall_metrics !== '' ? JSON.parse(data.overall_metrics) : {} + }; + // data.backhand = data.backhand !== '' ? JSON.parse(data.backhand) : {}; + // data.forehand = data.forehand !== '' ? JSON.parse(data.forehand) : {}; + // data.service = data.service !== '' ? JSON.parse(data.service) : {}; + // data.smash = data.smash !== '' ? JSON.parse(data.smash) : {}; + + return makeResponse( + res, + statusCodes.SUCCESS, + true, + 'Video uploaded successfully', + updatedData + ); + } catch (error) { + const code = error?.code || statusCodes.SERVER_ERROR; + const message = error?.message || responseMessages.SOMETHING_WRONG; + return makeResponse(res, code, false, message, {}); + } +}); + +export const uploadManualEntry = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + + // Validate required parameters in the request body + if (!req?.body?.accuracy || !req?.body?.shot || !req?.body?.consistency || !req?.body?.speed || !req?.body?.spinRateRight || !req?.body?.spinRateLeft || !req?.body?.duration) { + return makeResponse(res, statusCodes.BAD_REQUEST, 'Invalid Parameters', {}); // Return a BAD_REQUEST response if parameters are missing + } + + try { + // Fetch the current user details to ensure they exist + const currentUser = await getUserById(userId); + + // If the user is not found, return a NOT_FOUND response + if (!currentUser) { + return makeResponse( + res, + statusCodes.NOT_FOUND, + false, + responseMessages.USER_NOT_FOUND, + {} + ); + } + + // Get today's date using moment.js + const today = moment(); + + // Get the day of the week (e.g., Monday, Tuesday) + const dayOfWeek = today.format('dddd'); + + // Prepare an object to hold the updated fields for the upload + const updatedFields = { + user_id: userId, + file_name: '', + file_path: '', + file_type: '', + upload_day: dayOfWeek, + overall_metrics: '', + backhand: '', + forehand: '', + service: '', + smash: '', // Placeholder for smash data + total_duration: '', // Placeholder for total duration + }; + + // Prepare the manual data object for analysis + const manualData = { + "analysis_id": `${Date.now()}`, // Unique analysis ID based on current timestamp + "analysis_json": { + "fps": req?.body?.fps ?? null, // Frames per second, default to null if not provided + "shots": [ + { + "accuracy": req?.body?.accuracy ?? null, // Accuracy of the shot + "classification": req?.body?.shot ?? null, // Type of shot + "end_frame_idx": req?.body?.endFrame ?? null, // End frame index for the shot + "hand": req?.body?.hand ?? null, // Hand used for the shot + "shot_consistency": req?.body?.consistency ?? null, // Consistency of the shot + "speed": req?.body?.speed ?? null, // Speed of the shot + "spin_rate_left": req?.body?.spinRateLeft ?? null, // Spin rate for left spin + "spin_rate_right": req?.body?.spinRateRight ?? null, // Spin rate for right spin + "spin_type_left": req?.body?.spinRateLeftType ?? null, // Type of left spin + "spin_type_right": req?.body?.spinRateRightType ?? null, // Type of right spin + "start_frame_idx": req?.body?.frameIdx ?? null // Start frame index for the shot + } + ], + "total_duration": req?.body?.duration // Total duration of the video + } + }; + + // Perform AI analysis on the manual data + const aiAnalysisData = await getAIManualAnalysis(manualData); + + // If AI analysis data is returned, update the fields accordingly + if (aiAnalysisData) { + updatedFields.overall_metrics = aiAnalysisData.overall_metrics; // Update overall metrics + updatedFields.total_duration = aiAnalysisData.total_duration; // Update total duration + Object.assign(updatedFields, aiAnalysisData.shots); // Merge shot data into updated fields + } + + // Check if overall metrics were not updated + if (updatedFields?.overall_metrics === '') { + const code = statusCodes.BAD_REQUEST; // Set response code to BAD_REQUEST + const message = "Please upload the tennis specific video."; // Message for the user + return makeResponse(res, code, false, message, {}); // Return error response + } + + // Save the updated data to the database + const data = await createUpload(updatedFields); + + // Prepare the updated data to return in the response + const updatedData = { + id: data?.id, // ID of the uploaded data + user_id: data?.user_id, // User ID associated with the upload + file_path: data?.file_path + ? privateKey.NODE_SERVER_URL + data?.file_path // Construct full file path if available + : '', + total_duration: data?.total_duration, // Total duration of the upload + upload_time: data?.upload_time, // Time of upload + upload_day: data?.upload_day, // Day of upload + overall_metrics: data?.overall_metrics ? JSON.parse(data?.overall_metrics) : {}, // Parse overall metrics if available + }; + + // Return success response with updated data + return makeResponse( + res, + statusCodes.SUCCESS, + true, + 'Manual form submitted successfully', // Success message + updatedData // Include the updated data in the response + ); + } catch (error) { + // Handle any errors that occur during the process + const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code + const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message + + // Return error response with appropriate status and message + return makeResponse(res, code, false, message, {}); + } +}); + +export const getUserRecorderVideo = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + + try { + // Fetch current user details to check for changes + const currentUser = await getUserById(userId); + + // If the user is not found, return a NOT_FOUND response + if (!currentUser) { + return makeResponse( + res, + statusCodes.NOT_FOUND, + false, + responseMessages.USER_NOT_FOUND, + {} + ); + } + + // Fetch the user's uploaded data + const updatedData = await getUserUpload(userId); + + // Remove the password from the user data for security reasons + delete updatedData.dataValues.password; + + // Construct the full URL for the profile picture if it exists + updatedData.profile_pic = updatedData?.dataValues?.profile_pic ? privateKey.NODE_SERVER_URL + updatedData?.dataValues?.profile_pic : ""; + + // Filter and map the uploads to create simplified data + updatedData.dataValues.simplifiedData = updatedData.uploads + .filter((data) => { + // Only keep records where overall_metrics exists and is valid + try { + const analysisData = JSON.parse(data.overall_metrics)?.analysis_json.shots; + return analysisData && Array.isArray(analysisData) && analysisData.length > 0; + } catch (e) { + // In case of JSON parsing error, exclude this record + return false; + } + }) + .map((data) => { + // Parse the analysis data to extract feedback + const analysisData = JSON.parse(data.overall_metrics)?.analysis_json.shots; + let feedBack = ""; + + if (analysisData) { + // Accumulate feedback from each shot + analysisData.forEach((shot) => { + feedBack += shot.feedback + " "; // Concatenate feedback with a space + }); + } + + // Return a simplified object containing relevant data + return { + id: data.id, + user_id: data.user_id, + file_path: data.file_path ? privateKey.NODE_SERVER_URL + data.file_path : '', // Construct full file path + total_duration: data.total_duration, + upload_time: data.upload_time, + feedBack: feedBack.trim() // Optionally trim to remove trailing space + }; + }); + + delete updatedData.dataValues.uploads; + + // Return the updated user data (excluding password) + return makeResponse( + res, + statusCodes.SUCCESS, + true, + responseMessages.USER_UPDATED, + updatedData + ); + } catch (error) { + const code = error?.code || statusCodes.SERVER_ERROR; + const message = error?.message || responseMessages.SOMETHING_WRONG; + + return makeResponse(res, code, false, message, {}); + } +}); + + +export const getAnalysisOfVideo = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + + try { + // Check if both query parameter 'value' and route parameter 'id' are provided + if (!req.query.value && !req.params.id) { + return makeResponse( + res, + statusCodes.INVALID_REQUEST, + false, + responseMessages.INVALID_PARAMETERS, + {} + ); + } + + // Fetch current user details to verify their existence + const currentUser = await getUserById(userId); + + // If the user is not found, return a NOT_FOUND response + if (!currentUser) { + return makeResponse( + res, + statusCodes.NOT_FOUND, + false, + responseMessages.USER_NOT_FOUND, + {} + ); + } + + // Fetch the uploaded video data by user ID and video ID + const updatedData = await getUploadVideoById({ + user_id: userId, + id: req.params.id, + }); + + // Check if the requested analysis data exists in the fetched video data + if (updatedData.dataValues[req.query.value]) { + // Parse the analysis data if it exists + const result = JSON.parse(updatedData.dataValues[req.query.value]); + return makeResponse( + res, + statusCodes.SUCCESS, + true, + responseMessages.DATA_FETCHED, + result // Return the fetched analysis data + ); + } else { + // If the analysis data does not exist, return a success response with a failure message + return makeResponse( + res, + statusCodes.SUCCESS, + true, + responseMessages.SHOT_FAILURE, + {} + ); + } + } catch (error) { + // Handle any errors that occur during the process + const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code + const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message + + return makeResponse(res, code, false, message, {}); // Return error response + } +}); + + +// This will be the custom middleware to check the upload count +export const checkUploadCount = async (req, res, next) => { + try { + const { userId } = req.user; // Get userId from decoded JWT + + // Count the number of uploads for this user + const uploadCount = await countUserUploadsByUserId(userId); + + if (privateKey.NODE_ENV === 'development') { + // Define the maximum number of uploads allowed (e.g., 3 uploads) + const maxUploads = 15; + + if (uploadCount >= maxUploads) { + return res.status(400).json({ + message: `You have reached the maximum number of uploads (${maxUploads}).`, + }); + } + } + // If the upload count is within the limit, proceed to the next middleware + next(); + } catch (error) { + console.error('Error checking upload count:', error); + res + .status(500) + .json({ message: 'Server error while checking upload count.' }); + } +}; + +export const getUserLastRecorderVideo = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + + try { + // Fetch current user details to verify their existence + const currentUser = await getUserById(userId); + + // If the user is not found, return a NOT_FOUND response + if (!currentUser) { + return makeResponse( + res, + statusCodes.NOT_FOUND, + false, + responseMessages.USER_NOT_FOUND, + {} + ); + } + + // Fetch the uploaded videos for the user + const updatedData = await getUserUpload(userId); + + // Simplify the uploaded video data for easier access + const simplifiedData = updatedData?.uploads + ?.map((data) => ({ + id: data?.id, + user_id: data?.user_id, + file_path: data?.file_path + ? privateKey?.NODE_SERVER_URL + data?.file_path // Construct full file path + : '', + total_duration: data?.total_duration, + upload_time: data?.upload_time, + upload_day: data?.upload_day, // This should be like 'Monday', 'Tuesday', etc. + overall_metrics: data?.overall_metrics + ? JSON.parse(data?.overall_metrics) // Parse overall metrics if available + : {}, + })) + .sort((a, b) => a.id - b.id); // Sort the simplified data by video ID + + // Get the last uploaded video details if available + const lastUploadedVideo = + simplifiedData.length > 0 + ? { + id: simplifiedData[simplifiedData.length - 1].id, + user_id: simplifiedData[simplifiedData.length - 1].user_id, + file_path: simplifiedData[simplifiedData.length - 1].file_path, + total_duration: + simplifiedData[simplifiedData.length - 1].total_duration, + upload_time: simplifiedData[simplifiedData.length - 1].upload_time, + upload_day: simplifiedData[simplifiedData.length - 1].upload_day, // This should be like 'Monday', 'Tuesday', etc. + overall_metrics: + simplifiedData[simplifiedData.length - 1]?.overall_metrics // Access overall metrics of the last video + } + : {}; // If no videos, return an empty object + + // Get the second last uploaded video details if available + const lastSecondUploadedVideo = + simplifiedData.length > 1 + ? { + id: simplifiedData[simplifiedData.length - 2]?.id, + user_id: simplifiedData[simplifiedData?.length - 2]?.user_id, + file_path: simplifiedData[simplifiedData?.length - 2]?.file_path, + total_duration: + simplifiedData[simplifiedData?.length - 2]?.total_duration, + upload_time: simplifiedData[simplifiedData?.length - 2]?.upload_time, + upload_day: simplifiedData[simplifiedData?.length - 2]?.upload_day, // This should be like 'Monday', 'Tuesday', etc. + overall_metrics: + simplifiedData[simplifiedData?.length - 2]?.overall_metrics // Access overall metrics of the second last video + } + : {}; // If no second last video, return an empty object + + // Extract the serve accuracy from both videos + const lastServeAccuracy = lastUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "service")?.accuracy; + const secondLastServeAccuracy = lastSecondUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "service")?.accuracy; + + // Calculate the percentage increase in serve accuracy + const perIncServeAccuracy = ((lastServeAccuracy - secondLastServeAccuracy) / secondLastServeAccuracy) * 100; + + // Extract the backhand shot consistency from both videos + const lastBackhandShotConsistency = lastUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "backhand")?.shot_consistency; + const secondLastBackhandShotConsistency = lastSecondUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "backhand")?.shot_consistency; + + // Calculate the percentage increase in backhand shot consistency + const perIncBackhandShotConsistency = ((lastBackhandShotConsistency - secondLastBackhandShotConsistency) / secondLastBackhandShotConsistency) * 100; + + // Extract the shot speed from both videos + const lastShotSpeed = lastUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "smash")?.speed; + const secondLastShotSpeed = lastSecondUploadedVideo?.overall_metrics?.analysis_json.shots?.find(shot => shot.classification === "smash")?.speed; + + // Calculate the percentage increase in shot speed + const perIncShotSpeed = ((lastShotSpeed - secondLastShotSpeed) / secondLastShotSpeed) * 100; + + // Prepare a summary of the metrics + const summary = { + lastServeAccuracy: `${((lastServeAccuracy ?? 0.00) * 100).toFixed(2)}%`, // Format serve accuracy + perIncServeAccuracy: `${(isNaN(perIncServeAccuracy) ? 0.00 : perIncServeAccuracy).toFixed(2)}%`, // Format percentage increase in serve accuracy + lastBackhandShotConsistency: `${((lastBackhandShotConsistency ?? 0.00) * 100).toFixed(2)}%`, // Format backhand shot consistency + perIncBackhandShotConsistency: `${(isNaN(perIncBackhandShotConsistency) ? 0.00 : perIncBackhandShotConsistency).toFixed(2)}%`, // Format percentage increase in backhand shot consistency + lastShotSpeed: `${((lastShotSpeed ?? 0.00) * 100).toFixed(2)} km/h`, // Format shot speed + perIncShotSpeed: `${(isNaN(perIncShotSpeed) ? 0.00 : perIncShotSpeed).toFixed(2)} km/h`, // Format percentage increase in shot speed + } + + // Group records by upload_day (which is the day name like 'Monday', 'Tuesday', etc.) and count the occurrences + const recordsByDay = simplifiedData.reduce((acc, data) => { + const day = data.upload_day; // Group by upload_day (which is the day of the week name) + + if (!acc[day]) { + acc[day] = 0; // Initialize counter if this day isn't in the accumulator + } + acc[day]++; // Increment the counter for that day + + return acc; // Return the updated accumulator + }, {}); + + console.log(lastUploadedVideo) // Log the last uploaded video for debugging purposes + + // Calculate overall aggregate if uploads exist + const overallAggregate = updatedData?.uploads?.length > 0 ? await calculateOverallAggregate( + updatedData?.uploads + ) : {}; // Return an empty object if no uploads + + // Return the response with the details of the last uploaded video and summary + return makeResponse( + res, + statusCodes.SUCCESS, + true, + "Last User Video Details Fetched", // Success message + { + overallAggregatePercentage: overallAggregate, + lastUploadedVideo, + summary, + recordsByDay, + } + ); + } catch (error) { + // Handle any errors that occur during the process + const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code + const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message + + return makeResponse(res, code, false, message, {}); // Return error response + } +}); + + +export const getUserPerformanceStats = catchAsyncAction(async (req, res) => { + const { userId } = req.user; // Get userId from decoded JWT + + try { + // Fetch current user details to verify their existence + const currentUser = await getUserById(userId); + + // If the user is not found, return a NOT_FOUND response + if (!currentUser) { + return makeResponse( + res, + statusCodes.NOT_FOUND, + false, + responseMessages.USER_NOT_FOUND, + {} + ); + } + + // Get the current date + const currentDate = new Date(); + + // Calculate the start of the week (Monday) + const startOfWeek = new Date(currentDate.setDate(currentDate.getDate() - currentDate.getDay() + 1)); + startOfWeek.setHours(0, 0, 0, 0); // Set to the start of the day + + // Calculate the end of the week (Sunday) + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(startOfWeek.getDate() + 6); + endOfWeek.setHours(23, 59, 59, 999); // Set to the end of the day + + // Fetch the uploaded data for the current week + const currentWeekData = await getUserUploadByDateFilter(userId, startOfWeek, endOfWeek); + + // Calculate the start of the last week (Monday of the previous week) + const startOfLastWeek = new Date(startOfWeek); + startOfLastWeek.setDate(startOfWeek.getDate() - 7); // Go back 7 days + startOfLastWeek.setHours(0, 0, 0, 0); // Start of last week's Monday + + // Calculate the end of last week (Sunday of the previous week) + const endOfLastWeek = new Date(startOfLastWeek); + endOfLastWeek.setDate(startOfLastWeek.getDate() + 6); // Sunday of last week + endOfLastWeek.setHours(23, 59, 59, 999); // End of last week's Sunday + + // Fetch the uploaded data for the last week + const lastWeekData = await getUserUploadByDateFilter(userId, startOfLastWeek, endOfLastWeek); + + // Calculate overall aggregates for the current week if uploads exist + const overallCurrentWeekAggregate = currentWeekData?.uploads?.length > 0 ? await calculateOverallAggregate( + currentWeekData?.uploads + ) : {}; + + // Calculate overall aggregates for the last week if uploads exist + const overallLastWeekAggregate = lastWeekData?.uploads?.length > 0 ? await calculateOverallAggregate( + lastWeekData?.uploads + ) : {}; + + // Calculate percentage increases for various metrics + const perIncAccuracyOverall = ((overallCurrentWeekAggregate?.avgAccuracy - overallLastWeekAggregate?.avgAccuracy) / overallLastWeekAggregate?.avgAccuracy); + const perIncShotConsistencyOverall = ((overallCurrentWeekAggregate?.avgShotConsistency - overallLastWeekAggregate?.avgShotConsistency) / overallLastWeekAggregate?.avgShotConsistency); + const perIncShotSpeedOverall = ((overallCurrentWeekAggregate?.avgSpeed - overallLastWeekAggregate?.avgSpeed) / overallLastWeekAggregate?.avgSpeed); + const perIncSpinRateOverall = ((overallCurrentWeekAggregate?.avgSpinRateRight - overallLastWeekAggregate?.avgSpinRateRight) / overallLastWeekAggregate?.avgSpinRateRight); + + // Prepare a summary of the current week's performance metrics + const overallCurrentWeekSummary = { + perIncAccuracyOverall: `${(isNaN(perIncAccuracyOverall) ? 0.00 : perIncAccuracyOverall).toFixed(2)}%`, // Format accuracy increase + perIncShotConsistencyOverall: `${(isNaN(perIncShotConsistencyOverall) ? 0.00 : perIncShotConsistencyOverall).toFixed(2)}%`, // Format shot consistency increase + perIncSpinRateOverall: `${(isNaN(perIncSpinRateOverall) ? 0.00 : perIncSpinRateOverall).toFixed(2)}%`, // Format spin rate increase + perIncShotSpeedOverall: `${(isNaN(perIncShotSpeedOverall) ? 0.00 : perIncShotSpeedOverall).toFixed(2)} km/h`, // Format shot speed increase + } + + // Calculate overall improvement as an average percentage + const overallImprovement = ( + (perIncAccuracyOverall + perIncShotConsistencyOverall + perIncShotSpeedOverall + perIncSpinRateOverall) / 4 + ).toFixed(2); // Round to 2 decimal places + + // Add overall improvement to the summary + overallCurrentWeekSummary.overallImprovement = `${overallImprovement}%`; + + // Calculate metrics for the current and last week + const currentWeekMetrics = currentWeekData?.uploads?.length > 0 ? await calculateShotMetrics( + currentWeekData?.uploads + ) : {}; + const lastWeekMetrics = lastWeekData?.uploads?.length > 0 ? await calculateShotMetrics( + lastWeekData?.uploads + ) : {}; + + // Calculate percentage increases in specific shot metrics + const perIncServeAccuracy = ((currentWeekMetrics?.averageServiceAccuracy - lastWeekMetrics?.averageServiceAccuracy) / lastWeekMetrics?.averageServiceAccuracy) * 100; + const perIncBackhandConsistency = ((currentWeekMetrics?.averageBackhandConsistency - lastWeekMetrics?.averageBackhandConsistency) / lastWeekMetrics?.averageBackhandConsistency) * 100; + const perIncShotSpeed = ((currentWeekMetrics?.averageShotSpeed - lastWeekMetrics?.averageShotSpeed) / lastWeekMetrics?.averageShotSpeed) * 100; + + // Prepare a summary of the current week's shot metrics + const currentWeekSummary = { + currentWeekServeAccuracy: `${((currentWeekMetrics?.totalServiceAccuracy ?? 0.00) * 100).toFixed(2)}%`, // Format current week serve accuracy + perIncServeAccuracy: `${(isNaN(perIncServeAccuracy) ? 0.00 : perIncServeAccuracy).toFixed(2)}%`, // Format percentage increase in serve accuracy + currentWeekBackhandConsistency: `${((currentWeekMetrics?.totalBackhandConsistency ?? 0.00) * 100).toFixed(2)}%`, // Format current week backhand consistency + perIncBackhandConsistency: `${(isNaN(perIncBackhandConsistency) ? 0.00 : perIncBackhandConsistency).toFixed(2)}%`, // Format percentage increase in backhand consistency + currentWeekShotSpeed: `${((currentWeekMetrics?.averageShotSpeed ?? 0.00)).toFixed(2)} km/h`, // Format current week shot speed + perIncShotSpeed: `${(isNaN(perIncShotSpeed) ? 0.00 : perIncShotSpeed).toFixed(2)} km/h`, // Format percentage increase in shot speed + } + + // Return the response with the user performance statistics + return makeResponse( + res, + statusCodes.SUCCESS, + true, + "Details Fetched", // Success message + { + currentWeekSummary, + overallCurrentWeekSummary, + currentWeekServices: currentWeekMetrics?.services ?? [], // Include current week services + lastWeekServices: lastWeekMetrics?.services ?? [], // Include last week services + currentWeekSpins: currentWeekMetrics?.spinRightShots ?? [], // Include current week spin shots + lastWeekSpins: lastWeekMetrics?.spinRightShots ?? [] // Include last week spin shots + } + ); + } catch (error) { + // Handle any errors that occur during the process + const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code + const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message + + return makeResponse(res, code, false, message, {}); // Return error response + } +}); + + +async function calculateShotMetrics(uploads) { + // Initialize totals and separate arrays + let totalServiceAccuracy = 0; + let totalBackhandConsistency = 0; + let totalShotSpeed = 0; + + // Arrays to hold individual shot types + let services = []; + let spinRightShots = []; + + // Initialize counters for each shot type + let serviceCount = 0; + let backhandCount = 0; + let shotCount = 0; + + // Loop through each upload and extract shot data from overall_metrics + uploads?.forEach(upload => { + upload.overall_metrics = JSON.parse(upload?.overall_metrics); + const shots = upload?.overall_metrics?.analysis_json.shots; + + shots.forEach(shot => { + // Check for "service" shot type + if (shot?.classification === "service") { + totalServiceAccuracy += shot?.accuracy; + services.push(shot); // Add service shots to the 'services' array + serviceCount++; // Increment service count + } + + // Check for "backhand" shot type + if (shot?.classification === "backhand") { + totalBackhandConsistency += shot?.shot_consistency; + backhandCount++; // Increment backhand count + } + + // Check for "spin right" shot type + if (shot?.classification === "spin right") { + spinRightShots.push(shot); // Add spin right shots to the 'spinRightShots' array + } + + // Add the speed of every shot to the total speed + totalShotSpeed += shot?.speed; + shotCount++; // Increment total shot count + }); + }); + + // Calculate averages + const averageServiceAccuracy = serviceCount > 0 ? totalServiceAccuracy / serviceCount : 0; + const averageBackhandConsistency = backhandCount > 0 ? totalBackhandConsistency / backhandCount : 0; + const averageShotSpeed = shotCount > 0 ? totalShotSpeed / shotCount : 0; + + return { + totalServiceAccuracy, + totalBackhandConsistency, + totalShotSpeed, + averageServiceAccuracy, + averageBackhandConsistency, + averageShotSpeed, + services, // Returning the array of service shots + spinRightShots, // Returning the array of spin right shots + uploads + }; +} + +// Function to calculate aggregate metrics for a single video +async function calculateVideoMetrics(video) { + const analysisData = video?.overall_metrics ? JSON.parse(video?.overall_metrics)?.analysis_json.shots : []; + let totalAccuracy = 0; + let totalShotConsistency = 0; + let totalSpeed = 0; + let totalSpinRateRight = 0; + let feedBack = ""; + + analysisData.forEach((shot) => { + totalAccuracy += shot.accuracy; + totalShotConsistency += shot.shot_consistency; + totalSpeed += shot.speed; + totalSpinRateRight += shot.spin_rate_right; + feedBack = feedBack + shot.feedback + " "; + }); + + const numberOfShots = analysisData.length; + + // Calculate the average for each metric + const avgAccuracy = totalAccuracy / numberOfShots; + const avgShotConsistency = totalShotConsistency / numberOfShots; + const avgSpeed = totalSpeed / numberOfShots; + const avgSpinRateRight = totalSpinRateRight / numberOfShots; + + return { + avgAccuracy, + avgShotConsistency, + avgSpeed, + avgSpinRateRight, + feedBack + }; +} + +// Function to calculate overall aggregate across all videos +async function calculateOverallAggregate(videoData) { + let totalAccuracy = 0; + let totalShotConsistency = 0; + let totalSpeed = 0; + let totalSpinRateRight = 0; + let totalShots = 0; + let feedback = ""; + + // Use Promise.all to process all videos concurrently + const metricsPromises = videoData?.map(async (video) => { + const { avgAccuracy, avgShotConsistency, avgSpeed, avgSpinRateRight, feedBack } = + await calculateVideoMetrics(video); + + totalAccuracy += avgAccuracy; + totalShotConsistency += avgShotConsistency; + totalSpeed += avgSpeed; + totalSpinRateRight += avgSpinRateRight; + feedback = feedBack + + totalShots++; + }); + + // Wait for all promises to resolve + await Promise.all(metricsPromises); + + // Calculate overall aggregate for all videos + return { + avgAccuracy: ((totalAccuracy / totalShots)).toFixed(2), + avgShotConsistency: ((totalShotConsistency / totalShots)).toFixed(2), + avgSpeed: ((totalSpeed / totalShots)).toFixed(2), + avgSpinRateRight: ((totalSpinRateRight / totalShots)).toFixed(2), + feedback + }; +} + +export const isQuizPlayed = catchAsyncAction(async (req, res) => { + try { + const { userId } = req.user; // Get userId from decoded JWT + + // Fetch the user's answers to determine if the quiz has been played + const QuizPlayed = await getUserAnswersByUserId(userId); + + // Prepare response data indicating whether the quiz has been played + const responseData = { + isQuizPlayed: QuizPlayed.length > 0 ? true : false // Set to true if there are answers, otherwise false + }; + + // Return the response with the quiz played status + return makeResponse( + res, + statusCodes.SUCCESS, + true, + responseMessages.DATA_FETCHED, // Success message + responseData // Include the response data + ); + } catch (error) { + // Handle any errors that occur during the process + const code = error?.code || statusCodes.SERVER_ERROR; // Determine the error code + const message = error?.message || responseMessages.SOMETHING_WRONG; // Determine the error message + + // Return error response with appropriate status and message + return makeResponse(res, code, false, message, {}); + } +}); diff --git a/server/helper/catchAsyncAction/index.js b/server/helper/catchAsyncAction/index.js new file mode 100755 index 0000000..4c372bd --- /dev/null +++ b/server/helper/catchAsyncAction/index.js @@ -0,0 +1,8 @@ +import { makeResponse, statusCodes } from "../index.js"; + +// Wrapper for catch block +export const catchAsyncAction = fn => { + return (req, res, next) => { + fn(req, res, next).catch((err) => makeResponse(res, statusCodes.BAD_REQUEST, false, err.message)); + }; +}; diff --git a/server/helper/index.js b/server/helper/index.js new file mode 100755 index 0000000..84c3c89 --- /dev/null +++ b/server/helper/index.js @@ -0,0 +1,5 @@ +export * from './makeResponse/index.js'; +export * from './catchAsyncAction/index.js'; +export * from './qrcode_generator/index.js'; +export * from './s3/index.js'; +export * from './jwt/index.js'; \ No newline at end of file diff --git a/server/helper/jwt/index.js b/server/helper/jwt/index.js new file mode 100644 index 0000000..cc27282 --- /dev/null +++ b/server/helper/jwt/index.js @@ -0,0 +1,59 @@ +import jwt from 'jsonwebtoken'; +import { privateKey } from '../../config/privateKeys.js'; +import { makeResponse } from '../makeResponse/index.js'; +const { sign, verify } = jwt; + + +// This function creates tokens based on user data and the rememberMe flag +export const generateToken = async (data, rememberMe) => { + // Set expiration times + const accessTokenExpiresIn = rememberMe ? '365d' : '1d'; // Access token expires in 365 days or 1 day + const refreshTokenExpiresIn = '7d'; // Refresh token expires in 7 days + + try { + const accessToken = sign({ data }, privateKey.TOKEN_SECRET, { expiresIn: accessTokenExpiresIn }); + const refreshToken = sign({ data }, privateKey.REFRESH_TOKEN_SECRET, { expiresIn: refreshTokenExpiresIn }); + + return { accessToken, refreshToken }; + } catch (error) { + throw new Error("Error generating tokens: " + error.message); + } +}; + + +// This function checks the validity of a given token using the appropriate secret +export const verifyToken = async (token, type = 'access') => { + try { + // Use different secrets for access token and refresh token + const secret = type === 'access' ? privateKey.TOKEN_SECRET : privateKey.REFRESH_TOKEN_SECRET; + + return verify(token, secret); + } catch (error) { + throw new Error("Token verification failed: " + error.message); + } +}; + +// This function handles the logic for refreshing access tokens using a valid refresh token +export const refreshAccessToken = async (req, res) => { + const { refreshToken } = req.body; + + if (!refreshToken) { + return makeResponse(res, 403, false, 'Refresh Token is required'); // Respond if no refresh token is provided + } + + try { + // Verify the refresh token + const decoded = await verifyToken(refreshToken, 'refresh'); + + // Generate new access token (and optionally a new refresh token) + const { accessToken, refreshToken: newRefreshToken } = await generateToken(decoded.data, true); // Example with rememberMe = true + + return makeResponse(res, 200, true, 'New access and refresh token', { + token: accessToken, + refreshToken: newRefreshToken // Optionally send a new refresh token + }); + } catch (error) { + console.log(error); + return makeResponse(res, 403, false, 'Invalid or expired refresh token'); // Handle invalid refresh token + } +}; diff --git a/server/helper/makeResponse/index.js b/server/helper/makeResponse/index.js new file mode 100755 index 0000000..4c59ada --- /dev/null +++ b/server/helper/makeResponse/index.js @@ -0,0 +1,51 @@ +export const responseMessages = { + USER_CREATED: "Users created successfully!", + USER_ALREADY_EXISTS: "Email is already in use", + USERNAME_ALREADY_EXISTS: "Username is already in use", + INVALID_CREDENTIALS: "Invalid Password", + USER_NOT_FOUND: "User doesn't exists", + UNAUTHORIZED: "Unauthorized", + SOMETHING_WRONG: "Something went wrong", + LOGIN_SUCCESS: "Login successfully", + USER_UPDATED: "User profile updated successfully", + + QUESTIONS_CREATED: "Questions created successfully!", + QUESTIONS_UPDATED: "Question updated successfully", + QUESTIONS_NOT_FOUND: 'Question not found', + QUESTIONS_FETCHED: "Questions Fetched Successfully", + + DATA_ADDED: "Data added successfully", + DATA_FETCHED: "Data fetched successfully", + INVALID_PARAMETERS: "Parameters are missing", + SHOT_FAILURE: "You haven't played this shot" +} + +export const notificationPayload = {} + +export const statusCodes = { + 'SUCCESS': 200, + 'RECORD_CREATED': 201, + 'BAD_REQUEST': 400, + 'AUTH_ERROR': 401, + 'FORBIDDEN': 403, + 'NOT_FOUND': 404, + 'INVALID_REQUEST': 405, + 'RECORD_ALREADY_EXISTS': 409, + 'SERVER_ERROR': 500, + 'UNAUTHORIZED': 400 +} + +const makeResponse = async (res, statusCode, success, message, payload = null, meta = {}) => + new Promise(resolve => { + res.status(statusCode) + .send({ + success, + code: statusCode, + message, + data: payload, + meta + }); + resolve(statusCode); + }); + +export { makeResponse }; diff --git a/server/helper/qrcode_generator/index.js b/server/helper/qrcode_generator/index.js new file mode 100755 index 0000000..0f4f28a --- /dev/null +++ b/server/helper/qrcode_generator/index.js @@ -0,0 +1,12 @@ +import QRCode from 'qrcode' + +//Genrate QR code +const generateQR = async text => { + try { + return QRCode.toDataURL(text); + } catch (err) { + console.error(err) + } +} + +export default generateQR; \ No newline at end of file diff --git a/server/helper/s3/index.js b/server/helper/s3/index.js new file mode 100755 index 0000000..99bc1d6 --- /dev/null +++ b/server/helper/s3/index.js @@ -0,0 +1,35 @@ +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; +import { privateKey } from '../../config/privateKeys.js'; + +const useGetSignedUrl = (key,bucketName) => { + const client = new S3Client({ + credentials: { + accessKeyId: privateKey.S3_ACCESS_KEY, + secretAccessKey: privateKey.S3_SECRET_KEY + }, + region: privateKey.S3_REGION + }) + + const getUrl = async (key) => { + try { + const url = await getSignedUrl( + client, + new GetObjectCommand({ + Bucket: bucketName, + Key: key, + }), + { expiresIn: 3600 } + ); + return url; + } catch (error) { + return error; + } + }; + return getUrl(key); + +}; + +export { + useGetSignedUrl +} \ No newline at end of file diff --git a/server/loaders/db/index.js b/server/loaders/db/index.js new file mode 100755 index 0000000..d1f5104 --- /dev/null +++ b/server/loaders/db/index.js @@ -0,0 +1,23 @@ +import { Sequelize, DataTypes } from 'sequelize'; +import { privateKey } from '../../config/privateKeys.js'; // Make sure your DB string is stored here + +// Create a new Sequelize instance with the PostgreSQL connection string +const sequelize = new Sequelize(privateKey.DB_STRING_DEV, { + dialect: 'postgres', // The database dialect + logging: false, // Disable logging (optional) +}); + +// Test the connection to ensure it's working +const connectDb = async () => { + try { + await sequelize.authenticate(); + console.log('Database connection established'); + } catch (err) { + console.error('Error connecting to the database:', err); + } +}; + +connectDb(); + +// Export the Sequelize instance to use in other parts of the application +export { sequelize }; diff --git a/server/loaders/index.js b/server/loaders/index.js new file mode 100755 index 0000000..06834f7 --- /dev/null +++ b/server/loaders/index.js @@ -0,0 +1 @@ +export * from './db/index.js'; \ No newline at end of file diff --git a/server/middlewares/auth.js b/server/middlewares/auth.js new file mode 100644 index 0000000..90d4bf6 --- /dev/null +++ b/server/middlewares/auth.js @@ -0,0 +1,39 @@ +import { statusCodes, makeResponse, responseMessages } from '../helper/index.js'; +import { verifyToken } from '../helper/index.js'; + +// Middleware function for authentication +export default async function auth(req, res, next) { + try { + // Retrieve the token from the request headers + const token = req.headers["Authorization"] || req.headers["authorization"]; + + // Check if token is present or not + if (!token) { + + return makeResponse(res, statusCodes.AUTH_ERROR, false, responseMessages.UNAUTHORIZED); + } + + // Verify the token using the verifyToken function + const decode = await verifyToken(token, 'access'); + + // Verify if token is valid or not + if (!decode) { + return makeResponse(res, statusCodes.AUTH_ERROR, false, responseMessages.UNAUTHORIZED); + } + + // Check if the decoded token contains an email + if (decode.data?.email == null || decode.data?.email == undefined) { + + return makeResponse(res, statusCodes.AUTH_ERROR, false, responseMessages.UNAUTHORIZED); + } + + // Attach the user data to the request object + req.user = decode.data; + + + next(); + } catch (err) { + + return makeResponse(res, statusCodes.AUTH_ERROR, false, err.message); + } +}; diff --git a/server/middlewares/upload.js b/server/middlewares/upload.js new file mode 100644 index 0000000..4c50774 --- /dev/null +++ b/server/middlewares/upload.js @@ -0,0 +1,70 @@ +import multer from "multer"; +import { diskStorage } from "multer"; +import path from 'path'; + +// Allowed video file extensions +const allowedVideoTypes = ['.mp4', '.mov', '.avi', '.temp']; + +// Filter to check if the file is an image or a video (with specific extensions) +const multerFilter = (req, file, cb) => { + // Check if the file is an image + if (file.mimetype.startsWith("image")) { + cb(null, true); + } else if (file.mimetype.startsWith("video")) { + // Check if the video has a valid extension (.mp4, .mov, .avi) + const extname = path.extname(file.originalname).toLowerCase(); + if (allowedVideoTypes.includes(extname)) { + cb(null, true); + } else { + cb(new Error("Only .temp, .mp4, .mov, or .avi video files are allowed."), false); // Reject invalid video types + } + } else { + cb(new Error("Please upload only images or videos."), false); // Reject if neither image nor video + } +}; + +// Storage settings +const storage = diskStorage({ + destination: (req, file, cb) => { + // Dynamically set the destination folder based on MIME type + if (file.mimetype.startsWith("image")) { + cb(null, './public/images'); + } else if (file.mimetype.startsWith("video")) { + cb(null, './public/videos'); + } + }, + filename: (req, file, cb) => { + + const originalName = path.basename(file.originalname, path.extname(file.originalname)); + + + const customFileName = `${originalName}_${Date.now()}${path.extname(file.originalname)}`; + + + cb(null, customFileName); // E.g., "myImage_1634567890123.jpg" or "videoClip_1634567890123.mp4" + } + +}); + + +const upload = multer({ + storage: storage, + fileFilter: multerFilter, + limits: { + fileSize: 1024 * 1024 * 50 // 20MB file size limit + } +}); + +// Error handling middleware for multer +export const multerErrorHandler = (err, req, res, next) => { + if (err instanceof multer.MulterError) { + // If the error is related to file size or other Multer error + return res.status(400).json({ message: err.message }); + } else if (err) { + + return res.status(400).json({ message: err.message }); + } + next(); +} + +export default upload; diff --git a/server/migrations/20241212070757-add-metrics-to-user-uploads.cjs b/server/migrations/20241212070757-add-metrics-to-user-uploads.cjs new file mode 100644 index 0000000..20ec23b --- /dev/null +++ b/server/migrations/20241212070757-add-metrics-to-user-uploads.cjs @@ -0,0 +1,123 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + const { NODE_ENV } = process.env; // Get the current environment from NODE_ENV + + // Conditional logic based on environment + if (NODE_ENV === 'development' || NODE_ENV === 'staging') { + console.log(`Applying migrations for ${NODE_ENV} environment...`); + + await queryInterface.changeColumn('user_uploads', 'file_name', { + type: Sequelize.TEXT, + allowNull: false, // You can keep it as required, adjust as needed + }); + + // Change the 'file_name' column to TEXT type + await queryInterface.changeColumn('user_uploads', 'file_path', { + type: Sequelize.TEXT, + allowNull: false, // You can keep it as required, adjust as needed + }); + // Change the 'file_name' column to TEXT type + await queryInterface.changeColumn('user_uploads', 'file_type', { + type: Sequelize.TEXT, + allowNull: false, // You can keep it as required, adjust as needed + }); + // Change the 'file_name' column to TEXT type + await queryInterface.changeColumn('user_uploads', 'overall_metrics', { + type: Sequelize.TEXT, + allowNull: false, // You can keep it as required, adjust as needed + }); + // Change the 'file_name' column to TEXT type + await queryInterface.changeColumn('user_uploads', 'backhand', { + type: Sequelize.TEXT, + allowNull: false, // You can keep it as required, adjust as needed + }); + await queryInterface.changeColumn('user_uploads', 'service', { + type: Sequelize.TEXT, + allowNull: false, // You can keep it as required, adjust as needed + }); + await queryInterface.changeColumn('user_uploads', 'smash', { + type: Sequelize.TEXT, + allowNull: false, // You can keep it as required, adjust as needed + }); + await queryInterface.changeColumn('user_uploads', 'forehand', { + type: Sequelize.TEXT, + allowNull: false, // You can keep it as required, adjust as needed + }); + + // Add columns only for development or staging environments + await queryInterface.addColumn('user_uploads', 'overall_metrics', { + type: Sequelize.STRING, + allowNull: true, + }); + await queryInterface.addColumn('user_uploads', 'backhand', { + type: Sequelize.STRING, + allowNull: true, + }); + await queryInterface.addColumn('user_uploads', 'forehand', { + type: Sequelize.STRING, + allowNull: true, + }); + await queryInterface.addColumn('user_uploads', 'service', { + type: Sequelize.STRING, + allowNull: true, + }); + await queryInterface.addColumn('user_uploads', 'smash', { + type: Sequelize.STRING, + allowNull: true, + }); + } + + // Optionally, you could apply the columns to the production environment as well: + if (NODE_ENV === 'production') { + console.log('Applying migrations for production environment...'); + // You can add production-specific migration logic if needed here. + // For now, it's the same as dev and staging + await queryInterface.addColumn('user_uploads', 'overall_metrics', { + type: Sequelize.STRING, + allowNull: true, + }); + await queryInterface.addColumn('user_uploads', 'backhand', { + type: Sequelize.STRING, + allowNull: true, + }); + await queryInterface.addColumn('user_uploads', 'forehand', { + type: Sequelize.STRING, + allowNull: true, + }); + await queryInterface.addColumn('user_uploads', 'service', { + type: Sequelize.STRING, + allowNull: true, + }); + await queryInterface.addColumn('user_uploads', 'smash', { + type: Sequelize.STRING, + allowNull: true, + }); + } + }, + + down: async (queryInterface, Sequelize) => { + const { NODE_ENV } = process.env; // Get the current environment from NODE_ENV + + // Remove columns based on the environment + if (NODE_ENV === 'development' || NODE_ENV === 'staging') { + console.log(`Rolling back migrations for ${NODE_ENV} environment...`); + await queryInterface.removeColumn('user_uploads', 'overall_metrics'); + await queryInterface.removeColumn('user_uploads', 'backhand'); + await queryInterface.removeColumn('user_uploads', 'forehand'); + await queryInterface.removeColumn('user_uploads', 'service'); + await queryInterface.removeColumn('user_uploads', 'smash'); + } + + // Optionally, you could apply the rollback for production as well: + if (NODE_ENV === 'production') { + console.log('Rolling back migrations for production environment...'); + await queryInterface.removeColumn('user_uploads', 'overall_metrics'); + await queryInterface.removeColumn('user_uploads', 'backhand'); + await queryInterface.removeColumn('user_uploads', 'forehand'); + await queryInterface.removeColumn('user_uploads', 'service'); + await queryInterface.removeColumn('user_uploads', 'smash'); + } + } +}; diff --git a/server/model/assessment.js b/server/model/assessment.js new file mode 100644 index 0000000..d203279 --- /dev/null +++ b/server/model/assessment.js @@ -0,0 +1,40 @@ +import { sequelize } from './../loaders/index.js'; +import { DataTypes } from 'sequelize'; + +const assessmentQuestion = sequelize.define('assessmentQuestion', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + orderId: { + type: DataTypes.INTEGER, + allowNull: false, + + }, + question: { + type: DataTypes.STRING, + allowNull: false, + }, + options: { + type: DataTypes.TEXT, + allowNull: true, + } +}, { + tableName: 'assessment_questions', // Define the name of the table +}); + +// Sync the model with the database +const syncDb = async () => { + try { + await sequelize.sync({ alter: false }); + console.log('assessmentQuestion table created (or already exists)'); + } catch (err) { + console.error('Error syncing the database:', err); + } +}; + +// Call the sync function +syncDb(); + +export { assessmentQuestion }; diff --git a/server/model/training.js b/server/model/training.js new file mode 100644 index 0000000..01df994 --- /dev/null +++ b/server/model/training.js @@ -0,0 +1,56 @@ +import { sequelize } from './../loaders/index.js'; +import { DataTypes } from 'sequelize'; + +const Training = sequelize.define('training', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + + }, + drills: { + type: DataTypes.ARRAY(DataTypes.STRING), // Defines an array of strings, + allowNull: true, + }, + recommendation: { + type: DataTypes.TEXT, + allowNull: true, + }, + videos: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true, + }, + yoga_exercises: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true, + }, + weekly_plan: { + type: DataTypes.JSONB, // Defines a JSONB column, + allowNull: true, + }, + videos_watched: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: true, + }, +}, { + tableName: 'training_content', // Define the name of the table +}); + +// Sync the model with the database +const syncDb = async () => { + try { + await sequelize.sync({ alter: false }); + console.log('training table created (or already exists)'); + } catch (err) { + console.error('Error syncing the database:', err); + } +}; + +// Call the sync function +syncDb(); + +export { Training }; diff --git a/server/model/user.js b/server/model/user.js new file mode 100644 index 0000000..3a5f35d --- /dev/null +++ b/server/model/user.js @@ -0,0 +1,70 @@ +import { sequelize } from './../loaders/index.js'; // Import the sequelize instance +import { DataTypes } from 'sequelize'; + +// Define the 'User' model +const User = sequelize.define('User', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + username: { + type: DataTypes.STRING(100), + allowNull: false, + }, + full_name: { + type: DataTypes.STRING(100), + allowNull: false, + }, + email: { + type: DataTypes.STRING(255), + allowNull: false, + unique: true, // Email should be unique + validate: { + isEmail: true, // Validate email format + }, + }, + password: { + type: DataTypes.STRING(255), + allowNull: false, // Password must be provided + }, + profile_pic: { + type: DataTypes.TEXT, // Profile picture can be a URL or base64 string + allowNull: true, + }, + phone_number: { + type: DataTypes.STRING(15), + allowNull: true, // Phone number is optional + }, + age: { + type: DataTypes.STRING(15), + allowNull: true, + }, + gender: { + type: DataTypes.STRING(15), + allowNull: true, + }, + location: { + type: DataTypes.STRING(255), + allowNull: true, + } +}, { + tableName: 'users', // Define the name of the table + timestamps: true, +}); + +// Sync the model with the database (creates the table if it doesn't exist) +const syncDb = async () => { + try { + await sequelize.sync({ force: false }); + console.log('User table created (or already exists)'); + } catch (err) { + console.error('Error syncing the database:', err); + } +}; + +// Call the sync function +syncDb(); + +// Export the model to use it in other parts of your application +export { User }; diff --git a/server/model/userAnswered.js b/server/model/userAnswered.js new file mode 100644 index 0000000..6a5d608 --- /dev/null +++ b/server/model/userAnswered.js @@ -0,0 +1,48 @@ +import { sequelize } from './../loaders/index.js'; +import { DataTypes } from 'sequelize'; +import { User } from './user.js'; // Assuming your User model is in a file named user.model.js + + +const UserAnswers = sequelize.define('UserAnswers', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', // The name of the referenced table (ensure it matches the table name) + key: 'id', // The primary key of the User model + }, + onDelete: 'CASCADE', // If the user is deleted, their answers are also deleted + }, + answers: { + type: DataTypes.TEXT, + allowNull: false, + } +}, { + tableName: 'user_answers', // Define the name of the table + timestamps: true, // If you don't want Sequelize to automatically create `createdAt` and `updatedAt` columns +}); + +UserAnswers.belongsTo(User, { + foreignKey: 'user_id', + as: 'user' // Optional alias for the reverse relation +}); + +// Sync the model with the database +const syncDbnow = async () => { + try { + await sequelize.sync({ force: false }); + console.log('User Answer table created (or already exists)'); + } catch (err) { + console.error('Error syncing the database:', err); + } +}; + +// Call the sync function +syncDbnow(); + +export { UserAnswers }; diff --git a/server/model/userUpload.js b/server/model/userUpload.js new file mode 100644 index 0000000..4426479 --- /dev/null +++ b/server/model/userUpload.js @@ -0,0 +1,99 @@ +import { sequelize } from './../loaders/index.js'; +import { DataTypes } from 'sequelize'; +import { User } from './user.js'; // Assuming your User model is in a file named user.model.js + +// Define the 'UserUploads' model +const UserUploads = sequelize.define( + 'UserUploads', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', // The name of the referenced table (ensure it matches the table name) + key: 'id', // The primary key of the User model + }, + onDelete: 'CASCADE', // If the user is deleted, their uploads are also deleted + }, + file_name: { + type: DataTypes.TEXT, + allowNull: false, + }, + file_path: { + type: DataTypes.TEXT, + allowNull: false, + }, + file_type: { + type: DataTypes.TEXT, + allowNull: false, + }, + total_duration: { + type: DataTypes.TEXT, + allowNull: true, + }, + upload_day: { + type: DataTypes.STRING(100), + allowNull: false, + }, + upload_time: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + overall_metrics: { + type: DataTypes.TEXT, + allowNull: true, + }, + backhand: { + type: DataTypes.TEXT, + allowNull: true, + }, + forehand: { + type: DataTypes.TEXT, + allowNull: true, + }, + service: { + type: DataTypes.TEXT, + allowNull: true, + }, + smash: { + type: DataTypes.TEXT, + allowNull: true, + }, + }, + { + tableName: 'user_uploads', // Define the name of the table + timestamps: false, // If you don't want Sequelize to automatically create `createdAt` and `updatedAt` columns + } +); + +// User model associations +User.hasMany(UserUploads, { + foreignKey: 'user_id', + onDelete: 'CASCADE', + as: 'uploads', // Add alias 'uploads' here +}); + +UserUploads.belongsTo(User, { + foreignKey: 'user_id', + as: 'user', // Optional alias for the reverse relation +}); + +// Sync the model with the database +const syncDb = async () => { + try { + await sequelize.sync({ force: false }); + console.log('UserUploads table created (or already exists)'); + } catch (err) { + console.error('Error syncing the database:', err); + } +}; + +// Call the sync function +syncDb(); + +export { UserUploads }; diff --git a/server/models/index.js b/server/models/index.js new file mode 100644 index 0000000..024200e --- /dev/null +++ b/server/models/index.js @@ -0,0 +1,43 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const process = require('process'); +const basename = path.basename(__filename); +const env = process.env.NODE_ENV || 'development'; +const config = require(__dirname + '/../config/config.json')[env]; +const db = {}; + +let sequelize; +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config); +} else { + sequelize = new Sequelize(config.database, config.username, config.password, config); +} + +fs + .readdirSync(__dirname) + .filter(file => { + return ( + file.indexOf('.') !== 0 && + file !== basename && + file.slice(-3) === '.js' && + file.indexOf('.test.js') === -1 + ); + }) + .forEach(file => { + const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); + db[model.name] = model; + }); + +Object.keys(db).forEach(modelName => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/server/routes/assessment.js b/server/routes/assessment.js new file mode 100644 index 0000000..33744ca --- /dev/null +++ b/server/routes/assessment.js @@ -0,0 +1,14 @@ +import express from "express"; +import { addAssessmentQuestions, getAssessmentQuestions, updateAssessmentQuestionById, getUserAnswers, addUserAssessmentResponse, getUserDayTraining, updateVideoWatchByUser } from "../controller/assessment/index.js"; +import auth from "../middlewares/auth.js"; + +const router = express.Router(); + +router.post("/questions", addAssessmentQuestions); +router.patch("/update-question/:id", updateAssessmentQuestionById ); +router.get("/get-all-questions", getAssessmentQuestions); +router.post("/add-user-answers", auth, addUserAssessmentResponse); +router.get("/get-user-answers", auth, getUserAnswers); +router.get("/get-day-wise-training", auth, getUserDayTraining); +router.patch("/video-watch-for-day", auth, updateVideoWatchByUser); +export default router; diff --git a/server/routes/index.js b/server/routes/index.js new file mode 100644 index 0000000..6ea0825 --- /dev/null +++ b/server/routes/index.js @@ -0,0 +1,15 @@ +import express from 'express'; +import userRoute from './users/user.js'; +import assessmentRoute from './assessment.js'; + +const router = express.Router(); + +/* GET home page. */ +router.get('/', function (req, res, next) { + res.render('index', { title: 'Express' }); +}); + +router.use('/users', userRoute); +router.use('/assessment', assessmentRoute); + +export default router; diff --git a/server/routes/users/user.js b/server/routes/users/user.js new file mode 100644 index 0000000..2adcf54 --- /dev/null +++ b/server/routes/users/user.js @@ -0,0 +1,58 @@ +import express from "express"; +import { + checkUploadCount, + getAnalysisOfVideo, + getUserLastRecorderVideo, + getUserPerformanceStats, + getUserRecorderVideo, + isQuizPlayed, + updateUserProfileHandler, + updateUserRecorderMiniVideo, + updateUserRecorderVideo, + uploadManualEntry, + userLogin, + userSignUp +} from "../../controller/users/index.js"; +import auth from "../../middlewares/auth.js"; +import upload from "../../middlewares/upload.js"; +import { refreshAccessToken } from "../../helper/index.js"; + +const router = express.Router(); // Create a new router instance + +// Route to refresh the access token +router.post('/refresh-token', refreshAccessToken); + +// Route for user sign-up +router.post('/sign-up', userSignUp); + +// Route for user login +router.post('/login-in', userLogin); + +// Route to update user profile, requires authentication and file upload +router.patch('/update-user-profile', auth, upload.fields([{ name: 'profilePic', maxCount: 1 }]), updateUserProfileHandler); + +// Route to upload a recorder video, requires authentication and checks upload count +router.post('/upload-recorder-video', auth, checkUploadCount, upload.fields([{ name: 'userVideo', maxCount: 1 }]), updateUserRecorderVideo); + +// Route to get user uploaded videos, requires authentication +router.get('/get-user-uploaded', auth, getUserRecorderVideo); + +// Route to get analysis by uploaded video ID, requires authentication +router.get('/get-analysis-by-uploaded-id/:id', auth, getAnalysisOfVideo); + +// Route to get the last recorded video, requires authentication +router.get('/last-recorded-video', auth, getUserLastRecorderVideo); + +// Route to get performance stats by week, requires authentication +router.get('/performance-stats-by-week', auth, getUserPerformanceStats); + +// Route to check if a quiz has been played by the user, requires authentication +router.get('/quiz-played-by-user', auth, isQuizPlayed); + +// Route to upload a manual entry, requires authentication +router.post('/upload-manual-entry', auth, uploadManualEntry); + +// Route to upload a mini recorder video, requires authentication and checks upload count +router.post('/upload-mini-recorder-video', auth, checkUploadCount, upload.fields([{ name: 'userVideo', maxCount: 1 }]), updateUserRecorderMiniVideo); + +export default router; // Export the router for use in other parts of the application diff --git a/server/services/assessment.js b/server/services/assessment.js new file mode 100644 index 0000000..425ad49 --- /dev/null +++ b/server/services/assessment.js @@ -0,0 +1,94 @@ +import { Op } from "sequelize"; +import { assessmentQuestion } from "../model/assessment.js"; +import { Training } from "../model/training.js"; +import { User } from "../model/user.js"; +import { UserAnswers } from "../model/userAnswered.js"; + +export const createQuestions = async (payload) => { + try { + // Insert all questions at once using bulkCreate + const createdQuestions = await assessmentQuestion.bulkCreate(payload); + return createdQuestions; // Return the user object + } catch (err) { + console.error(err); + throw new Error('Error creating questions'); + } +}; + +export const getQuestionById = async (id) => { + try { + const question = await assessmentQuestion.findByPk(id); + return question; // Return the user object + } catch (err) { + console.error(err); + throw new Error('Error getting questions'); + } +}; + +export const updateQuestionById = async (question) => { + try { + return await question.save(); + + } catch (err) { + console.error(err); + throw new Error('Error getting questions'); + } +}; + +// Function to get all questions +export const getAllQuestions = async () => { + try { + return await assessmentQuestion.findAll({ + order: [['id', 'ASC']], + }); + } catch (err) { + console.error('Error fetching questions:', err); + throw new Error('Error fetching questions'); // Throw an error if something goes wrong + } +}; + +export const addUserAnswersByUserId = async (payload) => { + try { + const upload = await UserAnswers.create(payload); + return upload; + } catch (error) { + console.error('Error adding res:', error); + throw new Error('Error adding res'); + } +}; + +export const addUserDrillsByUserId = async (payload) => { + try { + const data = await Training.create(payload); + return data; + } catch (error) { + console.error('Error adding res drills:', error); + throw new Error('Error adding res drills'); + } +}; + +export const getUserAnswersByUserId = async (userId) => { + try { + // Find the user and include their uploads + const user = await UserAnswers.findAll({ + where: { user_id: userId }, // Filter by user ID + }); + return user; + } catch (error) { + console.error('Error uploading file:', error); + throw new Error('Error uploading file'); + } +}; + +export const getUserTrainingByUserId = async (userId) => { + try { + const userTraining = await Training.findOne({ + where: { user_id: userId }, // Filter by user ID + order: [['createdAt', 'DESC']], // Order by createdAt in descending order + }); + return userTraining; + } catch (error) { + console.error('Error Training:', error); + throw new Error('Error Training'); + } +}; diff --git a/server/services/users.js b/server/services/users.js new file mode 100644 index 0000000..edfe7dd --- /dev/null +++ b/server/services/users.js @@ -0,0 +1,156 @@ +import { Op } from "sequelize"; +import { User } from "../model/user.js"; +import { UserUploads } from "../model/userUpload.js"; + + +export const countUsers = async () => { + try { + const count = await User.count(); + return count; + } catch (error) { + throw new Error('Error counting users'); + } +}; + +// Function to get a user by email +export const getUserByEmail = async (email) => { + try { + const user = await User.findOne({ where: { email } }); + + return user; // Return the user object + } catch (err) { + console.error(err); + throw new Error('Error while checking user by email'); + } +}; + +export const getUserByFilter = async (payload) => { + try { + const user = await User.findOne({ where: payload }); + + return user; // Return the user object + } catch (err) { + console.error(err); + throw new Error('Error while checking user by filter'); + } +}; + +// Function to create a new user +export const createUser = async (payload) => { + try { + const user = await User.create(payload); + + return user; // Return the created user + } catch (err) { + console.error(err); + throw new Error('Error while creating user'); + } +}; + +// Function to get a user by ID (for example, to fetch profile data) +export const getUserById = async (id) => { + try { + const user = await User.findByPk(id); // Find the user by primary key (ID) + + return user; // Return the user data + } catch (err) { + console.error(err); + throw new Error('Error while fetching user by ID'); + } +}; + +export const countUserUploadsByUserId = async (userId) => { + try { + const count = await UserUploads.count({ + where: { + user_id: userId + } + }); + console.log(`Total uploads for user ID ${userId}: ${count}`); + return count; + } catch (error) { + console.error('Error counting user uploads:', error); + throw new Error('Error counting user uploads'); + } +}; + +// Update user profile +export const updateUserProfile = async (id, updatedFields) => { + try { + const user = await User.findByPk(id); + + // Dynamically update the fields based on the input + await user.update(updatedFields); // Sequelize handles the query and update + + return user; // Return the updated user + } catch (err) { + console.error(err); + throw new Error('Error while updating user profile'); + } +}; + +export const createUpload = async (payload) => { + try { + const upload = await UserUploads.create(payload); + return upload; + } catch (error) { + console.error('Error uploading file:', error); + throw new Error('Error uploading file'); + } +}; + +export const getUserUpload = async (userId) => { + try { + // Find the user and include their uploads + const user = await User.findOne({ + where: { id: userId }, // Filter by user ID + include: [ + { + model: UserUploads, // Include the user uploads + as: 'uploads', // Alias defined in associations + } + ] + }); + return user; + } catch (error) { + console.error('Error uploading file:', error); + throw new Error('Error uploading file'); + } +}; + +export const getUploadVideoById = async (payload) => { + try { + const userData = await UserUploads.findOne({ where: payload }); + + return userData; + } catch (error) { + console.error('Error file:', error); + throw new Error('Error file'); + } +}; + +export const getUserUploadByDateFilter = async (userId, startDate, endDate) => { + try { + + // Find the user and include their uploads + const user = await User.findOne({ + where: { id: userId }, // Filter by user ID + include: [ + { + model: UserUploads, // Include the user uploads + as: 'uploads', // Alias defined in associations + where: { + upload_time: { + [Op.between]: [startDate, endDate], // Filter uploads within the current week + } + } + } + ] + }); + + return user; + } catch (error) { + console.error('Error fetching user uploads:', error); + throw new Error('Error fetching user uploads'); + } +};