diff --git a/.aws/fargate-basic-task-revision1.json b/.aws/fargate-basic-task-revision1.json new file mode 100644 index 0000000..a9e05e3 --- /dev/null +++ b/.aws/fargate-basic-task-revision1.json @@ -0,0 +1,97 @@ +{ + "taskDefinitionArn": "arn:aws:ecs:us-east-2:621180867847:task-definition/fargate-basic-task:1", + "containerDefinitions": [ + { + "name": "app", + "image": "621180867847.dkr.ecr.us-east-2.amazonaws.com/portfolio-web-repo:latest", + "cpu": 0, + "portMappings": [ + { + "name": "app-80-tcp", + "containerPort": 80, + "hostPort": 80, + "protocol": "tcp", + "appProtocol": "http" + } + ], + "essential": true, + "restartPolicy": { + "enabled": true, + "restartAttemptPeriod": 300 + }, + "environment": [], + "environmentFiles": [], + "mountPoints": [], + "volumesFrom": [], + "ulimits": [], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/fargate-basic-task", + "awslogs-create-group": "true", + "awslogs-region": "us-east-2", + "awslogs-stream-prefix": "ecs" + }, + "secretOptions": [] + }, + "systemControls": [] + } + ], + "family": "fargate-basic-task", + "taskRoleArn": "arn:aws:iam::621180867847:role/ecsTaskExecutionRole", + "executionRoleArn": "arn:aws:iam::621180867847:role/ecsTaskExecutionRole", + "networkMode": "awsvpc", + "revision": 1, + "volumes": [], + "status": "ACTIVE", + "requiresAttributes": [ + { + "name": "com.amazonaws.ecs.capability.logging-driver.awslogs" + }, + { + "name": "ecs.capability.execution-role-awslogs" + }, + { + "name": "com.amazonaws.ecs.capability.ecr-auth" + }, + { + "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19" + }, + { + "name": "ecs.capability.container-restart-policy" + }, + { + "name": "com.amazonaws.ecs.capability.task-iam-role" + }, + { + "name": "ecs.capability.execution-role-ecr-pull" + }, + { + "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18" + }, + { + "name": "ecs.capability.task-eni" + }, + { + "name": "com.amazonaws.ecs.capability.docker-remote-api.1.29" + } + ], + "placementConstraints": [], + "compatibilities": [ + "EC2", + "FARGATE" + ], + "requiresCompatibilities": [ + "FARGATE" + ], + "cpu": "1024", + "memory": "3072", + "runtimePlatform": { + "cpuArchitecture": "X86_64", + "operatingSystemFamily": "LINUX" + }, + "registeredAt": "2025-08-04T18:34:06.217Z", + "registeredBy": "arn:aws:iam::621180867847:user/and-aws_@856", + "enableFaultInjection": false, + "tags": [] +} \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..3cb6e50 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,92 @@ +# This workflow will build and push a new container image to Amazon ECR, +# and then will deploy a new task definition to Amazon ECS, when there is a push to the "main" branch. +# +# To use this workflow, you will need to complete the following set-up steps: +# +# 1. Create an ECR repository to store your images. +# For example: `aws ecr create-repository --repository-name my-ecr-repo --region us-east-2`. +# Replace the value of the `ECR_REPOSITORY` environment variable in the workflow below with your repository's name. +# Replace the value of the `AWS_REGION` environment variable in the workflow below with your repository's region. +# +# 2. Create an ECS task definition, an ECS cluster, and an ECS service. +# For example, follow the Getting Started guide on the ECS console: +# https://us-east-2.console.aws.amazon.com/ecs/home?region=us-east-2#/firstRun +# Replace the value of the `ECS_SERVICE` environment variable in the workflow below with the name you set for the Amazon ECS service. +# Replace the value of the `ECS_CLUSTER` environment variable in the workflow below with the name you set for the cluster. +# +# 3. Store your ECS task definition as a JSON file in your repository. +# The format should follow the output of `aws ecs register-task-definition --generate-cli-skeleton`. +# Replace the value of the `ECS_TASK_DEFINITION` environment variable in the workflow below with the path to the JSON file. +# Replace the value of the `CONTAINER_NAME` environment variable in the workflow below with the name of the container +# in the `containerDefinitions` section of the task definition. +# +# 4. Store an IAM user access key in GitHub Actions secrets named `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. +# See the documentation for each action used below for the recommended IAM policies for this IAM user, +# and best practices on handling the access key credentials. + +name: CD Pipeline Amazon ECS + +on: + push: + branches: ["main"] + +env: + AWS_REGION: us-east-2 + ECR_REPOSITORY: portfolio-web-repo + ECS_SERVICE: fargate-basic-service-portfolio + ECS_CLUSTER: portfolio-web-cluster + ECS_TASK_DEFINITION: .aws/fargate-basic-task-revision1.json + CONTAINER_NAME: app + +permissions: + contents: read + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build, tag, and push image to Amazon ECR + id: build-image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_TAG: ${{ github.sha }} + run: | + # Build a docker container and + # push it to ECR so that it can + # be deployed to ECS. + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ${{ env.ECS_TASK_DEFINITION }} + container-name: ${{ env.CONTAINER_NAME }} + image: ${{ steps.build-image.outputs.image }} + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: ${{ env.ECS_SERVICE }} + cluster: ${{ env.ECS_CLUSTER }} + wait-for-service-stability: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b80d811 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI Pipeline +on: + push: + branches: + - staging + - main + pull_request: + branches: + - main + workflow_dispatch: + inputs: + branch_name: + description: "Branch to run tests" + required: true + default: "staging" + +jobs: + test: + runs-on: ubuntu-latest + name: Building and Testing + steps: + - uses: actions/checkout@v4 + + - name: Create NodeJS env + uses: actions/setup-node@v4 + with: + node-version: 22.x + + - name: Install packages + run: npm ci && npm ci --prefix frontend && npm ci --prefix backend + + - name: Run tests + run: npm test diff --git a/.gitignore b/.gitignore index 4e8870b..cb22775 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,9 @@ node_modules/ frontend/node_modules/ frontend/dist/ frontend/TODO.md +frontend/coverage -backend/node_modules/ \ No newline at end of file +backend/node_modules/ +backend/coverage +backend/logs/ +backend/uploads \ No newline at end of file diff --git a/README.md b/README.md index fd03aad..b34df8d 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ # My Portfolio Website +[![CI Pipeline](https://github.com/arauta12/MyWebsite/actions/workflows/ci.yml/badge.svg)](https://github.com/arauta12/MyWebsite/actions/workflows/ci.yml) diff --git a/backend/app.js b/backend/app.js index 5cbc3a1..29840ad 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,30 +1,56 @@ const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); - +const cookieParser = require('cookie-parser'); +const morgan = require('morgan'); +const path = require('path'); +const fs = require('fs'); const authRoute = require('./routes/auth.route'); +const userRoute = require('./routes/user.route'); const projectRoute = require('./routes/project.route'); const messageRoute = require('./routes/message.route'); +const uploadRoute = require('./routes/uploads.route'); const app = express(); +const whitelist = [ + "http://localhost:3000", + "http://localhost:5173", + "http://localhost:8080", +]; const corsConfig = { - origin: [ - "http://localhost:3000", - "http://localhost:5173", - ], + origin: whitelist, credentials: true, optionsSuccessStatus: 200 }; +const createLogName = () => { + const logDate = new Date(Date.now()); + const logName = `${logDate.getUTCMonth()}-${logDate.getUTCDay()}-${logDate.getUTCFullYear()}.log`; + console.log(logName); + + return path.join(__dirname, 'logs', logName); +}; + +const logStream = fs.createWriteStream( + createLogName(), + { autoClose: true, emitClose: false, flags: 'a' } +); + +const logger = morgan("combined", { immediate: true, stream: logStream }); + +app.use(logger); app.use(cors(corsConfig)); app.use(helmet()); app.use(express.json()); +app.use(cookieParser()); app.use('/api/auth', authRoute); app.use('/api/projects', projectRoute); app.use('/api/messages', messageRoute); +app.use('/api/users', userRoute); +// app.use('/api/uploads', uploadRoute); app.all('{*splat}', (req, res) => { res.status(404).send('That resource does not exist!'); diff --git a/backend/controllers/auth.controller.js b/backend/controllers/auth.controller.js index e187348..a7648bd 100644 --- a/backend/controllers/auth.controller.js +++ b/backend/controllers/auth.controller.js @@ -1,15 +1,152 @@ const User = require('../models/user.model'); -const userControl = require('../controllers/user.controller'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const chalk = require('chalk'); + +const verifyToken = async (req, res, next) => { + const jwtToken = req.cookies.jwt_token; + + if (!jwtToken) + return res.status(401).json({ status: "failed", message: "Unauthorized!" }); + + try { + const { exp, username, role } = jwt.verify(jwtToken, process.env.JWT_SECRET); + const existingUser = await User.findOne({ username, role }); + + if (!existingUser || !existingUser.loggedIn || (exp <= (Math.ceil(Date.now() / 1000)))) + return res.status(401).json({ status: "failed", message: "Unauthorized!" }); + + req.locals = role; + next(); + } catch (err) { + console.error(chalk.red(`ERROR Verify token: ${err.message}`)); + return res.status(500).json({ status: "failed", message: "Something went wrong! Try again." }); + } +}; + +const verifyAdmin = async (req, res, next) => { + if (!req.locals || req.locals !== "Admin") + return res.status(403).json({ status: "failed", message: "Unauthorized!" }); + + delete req.locals; + next(); +}; const signup = async (req, res) => { + const { data: { username, password, role = 'viewer' } } = req.body; + try { + if (!username || !password) + return res.status(400).json({ status: "failed", message: "Missing username or password!" }); + + const existingUser = await User.findOne({ username }, '-_id -__v'); + if (existingUser) + return res.status(400).json({ status: "failed", message: "User already exists!" }); + + const hashedPassword = bcrypt.hash(password, 10); + const newUser = await User.create({ username, hashedPassword, role, loggedIn: true }); + + // verification stuff & jwt + const token = await jwt.sign({ username, role }, process.env.JWT_SECRET, { + expiresIn: '10h' + }); + + return res.status(201).json({ status: "success", data: { username, role } }).cookie("jwt_token", token); + } catch (err) { + return res.status(500).json({ status: "failed", message: err.message }); + } }; const login = async (req, res) => { + const { data = {} } = req.body; + const { username = "", password = "" } = data; + + try { + const jwtToken = req.cookies.jwt_token; + + if (jwtToken) { + + const jwtData = jwt.verify(jwtToken, process.env.JWT_SECRET); + + const { exp, username, role } = jwtData; + const existingUser = await User.findOne({ username, role }); + + if (existingUser && existingUser.loggedIn) { + return res.status(200).json({ status: "success", data: { username: jwtData.username, role: jwtData.role } }); + } + } + } catch (err) { + res.clearCookie("jwt_token"); + } + + try { + if (!data || !username || !password) + return res.status(400).json({ status: "failed", message: "Missing username or password!" }); + + const user = await User.findOne({ username }); + + const passwordCompare = await bcrypt.compare(password, user?.password || ""); + + if (!user || !passwordCompare) + return res.status(401).json({ status: "failed", message: "Invalid credentials!" }); + + const token = jwt.sign({ username, role: user.role }, process.env.JWT_SECRET, { + expiresIn: '10h', + }); + + user.loggedIn = true; + await user.save(); + + res.clearCookie("jwt_token"); + res.cookie("jwt_token", token, { + maxAge: 1000 * 60 * 60 * 10, // 10hrs + secure: true, + httpOnly: true, + }); + + return res.status(200).json({ status: "success", data: { username, role: user.role } }); + } catch (err) { + console.error(chalk.red(`LOGIN ERROR: ${err.message}`)); + return res.status(500).json({ status: "failed", message: "Something went wrong. Try again." }); + } +}; + +const logout = async (req, res) => { + if (!req.cookies?.jwt_token) + return res.status(401).json({ status: "failed", message: "Invalid credentials." }); + + const jwtString = req.cookies.jwt_token; + + let jwtData = {}; + + try { + jwtData = jwt.verify(jwtString, process.env.JWT_SECRET); + + if (!jwtData?.username || !jwtData.role) + return res.status(401).json({ status: "failed", message: "Invalid credentials." }); + } catch (err) { + return res.status(401).json({ status: "failed", message: "Invalid credentials." }); + } + + try { + const user = await User.findOne({ username: jwtData.username, role: jwtData.role }); + if (!user) + return res.status(401).json({ status: "failed", message: "Invalid credentials." }); + + user.loggedIn = false; + res.clearCookie("jwt_token"); + return res.status(200).json({ status: "success", data: user.username }); + } catch (err) { + console.error(chalk.red(`ERROR Logout: ${err.message}`)); + return res.status(500).json({ status: "failed", message: "Something went wrong! Try again." }); + } }; module.exports = { signup, - login + login, + logout, + verifyToken, + verifyAdmin, }; \ No newline at end of file diff --git a/backend/controllers/message.controller.js b/backend/controllers/message.controller.js index 1b7b49a..00c55ff 100644 --- a/backend/controllers/message.controller.js +++ b/backend/controllers/message.controller.js @@ -1,4 +1,5 @@ const Message = require('../models/message.model'); +const chalk = require('chalk'); /** * Get all messages @@ -11,15 +12,19 @@ const getMessages = async (req, res) => { try { const messages = await Message.find({}, '-_id -__v'); - return res.status(200).json(messages); + return res.status(200).json({ status: "success", data: messages }); } catch (err) { - return res.status(500).json({ status: "failed", message: err.message }); + console.error(chalk.red(`MESSAGE ROUTE ERROR: ${err.message}!`)); + return res.status(500).json({ status: "failed", message: "Something went wrong. Try again." }); } }; const createMessage = async (req, res) => { - const { data = {} } = req.body; + if (!req.body?.data) + return res.status(400).json({ status: "failed", message: "Missing required fields!" }); + + const { data } = req.body; const { name, email, contents } = data; try { @@ -27,9 +32,10 @@ const createMessage = async (req, res) => { return res.status(400).json({ status: "failed", message: "Missing required fields!" }); await Message.create({ name, email, contents }); - return res.status(201).json({ status: "success", data: { name } }); + return res.status(201).json({ status: "success", data: name }); } catch (err) { - return res.status(500).json({ status: "failed", message: err.message }); + console.error(chalk.red(`MESSAGE ROUTE ERROR: ${err.message}!`)); + return res.status(500).json({ status: "failed", message: "Something went wrong. Try again." }); } }; diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js index 020d513..6607a56 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -1,19 +1,35 @@ const Project = require('../models/project.model'); +const chalk = require('chalk'); + +/** + * Get projects for public display + * + * @returns List of all public project objects + */ +const getDisplayProjects = async (req, res) => { + try { + const projects = await Project.find({ show: true }, '-_id -__v'); + return res.status(200).json({ status: "success", data: projects }); + } catch (err) { + console.error(chalk.red(`PROJECT ROUTE ERROR: ${err.message}!`)); + return res.status(500).json({ status: "failed", message: "Something went wrong. Try again." }); + } +}; /** * Get every project that exists * - * Filter is passed in the body to select particular ones - * @returns List of all project objects satisfying the conditions + * @returns List of all project objects */ const getAllProjects = async (req, res) => { // const { filter = "" } = req.body; try { - const projects = await Project.find({}, '-_id -__v'); - return res.status(200).json(projects); + const projects = await Project.find({}); + return res.status(200).json({ status: "success", data: projects }); } catch (err) { - return res.status(500).json({ status: "failed", message: err.message }); + console.error(chalk.red(`PROJECT ROUTE ERROR: ${err.message}!`)); + return res.status(500).json({ status: "failed", message: "Something went wrong. Try again." }); } }; @@ -22,13 +38,20 @@ const getAllProjects = async (req, res) => { * @returns All project data */ const getProject = async (req, res) => { + if (!req.params?.id) + return res.status(400).json({ status: "failed", message: "Missing id!" }); + const { id } = req.params; try { const project = await Project.findById(id); - return res.status(200).json(project); + if (!project) + return res.status(400).json({ status: "failed", message: "No such project!" }); + + return res.status(200).json({ status: "success", data: project }); } catch (err) { - return res.status(500).json({ status: "failed", message: err.message }); + console.error(chalk.red(`PROJECT ROUTE ERROR: ${err.message}!`)); + return res.status(500).json({ status: "failed", message: "Something went wrong. Try again." }); } }; @@ -37,17 +60,22 @@ const getProject = async (req, res) => { * @returns New project's name and show in a data object */ const createProject = async (req, res) => { - const { data = {} } = req.body; - const { name, image, description, link, show } = data; + if (!req.body?.data) + return res.status(400).json({ status: "failed", message: "Missing required fields!" }); + + const { data } = req.body; try { - if (!name || !description || !link) + if (!data?.name || !data.description || !data.link) return res.status(400).json({ status: "failed", message: "Missing required fields!" }); - await Project.create({ name, image, description, link, show }); - return res.status(201).json({ status: "success", data: { name, show } }); + const { name, image, description, link, show } = data; + + const newProject = await Project.create({ name, image, description, link, show }); + return res.status(201).json({ status: "success", data: { name, show, id: newProject._id } }); } catch (err) { - return res.status(500).json({ status: "failed", message: err.message }); + console.error(chalk.red(`PROJECT ROUTE ERROR: ${err.message}!`)); + return res.status(500).json({ status: "failed", message: "Something went wrong. Try again." }); } }; @@ -56,13 +84,20 @@ const createProject = async (req, res) => { * @returns Deleted project's name and show in a data object */ const deleteProject = async (req, res) => { + if (!req.params?.id) + return res.status(400).json({ status: "failed", message: "Missing id!" }); + const { id } = req.params; try { - const { name, show } = await Project.findByIdAndDelete(id); - return res.status(200).json({ status: "success", data: { name, show } }); + const deletedProject = await Project.findByIdAndDelete(id); + if (!deletedProject) + return res.status(400).json({ status: "failed", message: "No such project!" }); + + return res.status(200).json({ status: "success", data: { name: deletedProject.name, show: deletedProject.show } }); } catch (err) { - return res.status(500).json({ status: "failed", message: err.message }); + console.error(chalk.red(`PROJECT ROUTE ERROR: ${err.message}!`)); + return res.status(500).json({ status: "failed", message: "Something went wrong. Try again." }); } }; @@ -73,16 +108,28 @@ const deleteProject = async (req, res) => { * @returns Updated project's name and show in a data object */ const updateProject = async (req, res) => { + if (!req.params?.id) + return res.status(400).json({ status: "failed", message: "Missing id!" }); + const { id } = req.params; - const { data } = req.body; + const { data } = req.body ?? { data: {} }; + + const prohibitedFields = [ "_id", "__v", "createdAt", "updatedAt" ]; + + prohibitedFields.forEach(key => { + if (data && key in data) + delete data[ key ]; + }); try { - const project = await Project.findByIdAndUpdate(id, data); - console.log(data); + const project = await Project.findByIdAndUpdate(id, data, { new: true }); + if (!project) + return res.status(400).json({ status: "failed", message: "No such project!" }); - return res.status(200).json({ status: "success", data }); + return res.status(200).json({ status: "success", data: { name: project.name, show: project.show } }); } catch (err) { - return res.status(500).json({ status: "failed", message: err.message }); + console.error(chalk.red(`PROJECT ROUTE ERROR: ${err.message}!`)); + return res.status(500).json({ status: "failed", message: "Something went wrong. Try again." }); } }; @@ -91,5 +138,6 @@ module.exports = { getProject, createProject, deleteProject, - updateProject + updateProject, + getDisplayProjects }; diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js index e257f3e..282860d 100644 --- a/backend/controllers/user.controller.js +++ b/backend/controllers/user.controller.js @@ -1,23 +1,66 @@ const User = require('../models/user.model'); +const bcrypt = require('bcryptjs'); const getAllUsers = async (req, res) => { - + try { + const users = await Users.find({}, '-_id -__v'); + return res.status(200).json(users); + } catch (err) { + return res.status(500).json({ status: "failed", message: err.message }); + } }; const getUser = async (req, res) => { + const { id } = req.params; + + try { + if (!id) + return res.status(400).json({ status: "failed", message: "Missing id" }); + const user = await User.findById(id, '-_id -__v'); + return res.status(200).json(user); + } catch (err) { + return res.status(500).json({ status: "failed", message: err.message }); + } }; const createUser = async (req, res) => { + const { data = {} } = req.body; + const { username, password, role = 'viewer' } = data; + + try { + if (!username || !password) + return res.status(400).json({ status: "failed", message: "Missing required fields" }); + const hashedPassword = await bcrypt.hash(password, 10); + await User.create({ username, password: hashedPassword, role }); + return res.status(201).json({ status: "success", data: { username, role } }); + } catch (err) { + return res.status(500).json({ status: "failed", message: err.message }); + } }; const updateUser = async (req, res) => { + const { id } = req.params; + const { data = {} } = req.body; + try { + await User.findByIdAndUpdate(id, data); + return res.status(200).json({ status: "success", data }); + } catch (err) { + return res.status(500).json({ status: "failed", message: err.message }); + } }; const deleteUser = async (req, res) => { + const { id } = req.params; + try { + const { username, role } = await User.findByIdAndDelete(id); + return res.status(200).json({ status: "success", data: { username, role } }); + } catch (err) { + return res.status(500).json({ status: "failed", message: err.message }); + } }; diff --git a/backend/index.js b/backend/index.js index 1cacf26..7ab6e72 100644 --- a/backend/index.js +++ b/backend/index.js @@ -2,10 +2,10 @@ const { config } = require('dotenv'); const process = require('process'); const app = require('./app'); const { mongoose, connect } = require('./db/mongo'); + const chalk = require('chalk'); config(); - const PORT = process.env.PORT | 3000; const server = app.listen(PORT, (err) => { diff --git a/backend/models/project.model.js b/backend/models/project.model.js index 3056514..cca6aa0 100644 --- a/backend/models/project.model.js +++ b/backend/models/project.model.js @@ -6,7 +6,10 @@ const projectSchema = new mongoose.Schema({ required: true, default: "Unnamed" }, - image: String, + image: { + type: [ String ], + default: [] + }, description: { type: [ String ], required: true, diff --git a/backend/models/user.model.js b/backend/models/user.model.js index 69ce478..eaeb688 100644 --- a/backend/models/user.model.js +++ b/backend/models/user.model.js @@ -12,11 +12,15 @@ const userSchema = new mongoose.Schema({ }, role: { type: String, - default: "viewer", + default: "elite", enum: { - values: [ 'admin', 'viewer' ], + values: [ 'Admin', 'Elite' ], message: '{VALUE} is not a valid role!' } + }, + loggedIn: { + type: Boolean, + default: false } }); diff --git a/backend/multer/fileUpload.js b/backend/multer/fileUpload.js new file mode 100644 index 0000000..250372a --- /dev/null +++ b/backend/multer/fileUpload.js @@ -0,0 +1,72 @@ +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const fsPromise = require('fs').promises; +const { uploadFileFolder, uploadImageFolder } = require('./multerConfig'); + +const saveImage = (req, res, next) => { + if (!req.file) { + next('route'); + } else { + console.log(req.file); + next(); + } +}; + + +// TODO: update image attr in project to uid & name +const uploadImage = (req, res) => { + if (!req.locals) + return res.status(400).json({ status: "failed", message: "Missing fields!" }); + + const { uid = "", name = "" } = req.locals; + delete req.locals; + + if (!uid || !name) + return res.status(400).json({ status: "failed", message: "Missing fields!" }); + + if (!fs.existsSync(path.join(uploadImageFolder, `${uid}-${name}`))) + return res.status(400).json({ status: "failed", message: "File failed to tupload!" }); + + console.log(`${req.file} w/ uid: ${uid}`); + + res.status(201).json({ status: "success", data: { uid, name } }); +}; + +const downloadImage = async (req, res) => { + const { name } = req.params; + + const fileData = await fsPromise.readFile(path.join(uploadImageFolder, name)); + const dataArray = new Uint8Array(fileData); + + const blob = new Blob([ dataArray ], { type: `image/${name.split(".")[ 1 ]}` }); + const imageUrl = URL.createObjectURL(blob); + + console.log(imageUrl); + console.log(blob); + + return res.status(200).json({ status: "success", data: imageUrl }); +}; + +const downloadFile = async (req, res) => { + const { name } = req.params; + + const fileData = await fsPromise.readFile(path.join(uploadFileFolder, name)); + // console.log(fileData); + + console.log(typeof fileData); + + return res.status(200).json({ status: "success", data: fileData }); +}; + +const uploadFile = (req, res) => { + +}; + +module.exports = { + saveImage, + uploadImage, + downloadImage, + downloadFile, + uploadFile +}; \ No newline at end of file diff --git a/backend/multer/multerConfig.js b/backend/multer/multerConfig.js new file mode 100644 index 0000000..f044ea1 --- /dev/null +++ b/backend/multer/multerConfig.js @@ -0,0 +1,69 @@ +const path = require('path'); +const multer = require('multer'); +const uuid = require('uuid').v6; + +const uploadFolder = path.join(__dirname, '..', 'uploads'); +const uploadImageFolder = path.join(uploadFolder, 'images'); +const uploadFileFolder = path.join(uploadFolder, 'pdfs'); + +const imageStorage = multer.diskStorage({ + destination: (req, file, cb) => { + console.log(`destination: ${file}`); + cb(null, uploadImageFolder); + }, + filename: (req, file, cb) => { + const { originalname, mimetype } = file; + console.log(`CB: ${JSON.stringify(file)}`); + + if (!mimetype.includes("image")) { + cb(new Error("File is not an image!")); + } else { + const uid = uuid(); + const name = originalname; + req.locals = { uid, name }; + cb(null, `${uid}-${originalname}`); + } + + } +}); + +const fileStorage = multer.diskStorage({ + destination: (req, file, cb) => { + console.log(`destination: ${file}`); + + cb(null, uploadFileFolder); + }, + filename: (req, file, cb) => { + const { originalname } = file; + console.log(`CB: ${JSON.stringify(file)}`); + + const uid = uuid(); + const name = originalname; + req.locals = { uid, name }; + cb(null, `${uuid()}-${originalname}`); + + } +}); + +const uploadImageMulter = multer({ + storage: imageStorage, + // limits: { + // fileSize: 1024 * 1024, + // files: 1, + // fields: 10 + // } +}); + +const uploadFileMulter = multer({ + storage: fileStorage, + // limits: { + // fileSize: 1024 * 1024, + // files: 1, + // fields: 10 + // } +}); + +module.exports = { + uploadImageMulter, uploadFileMulter, + uploadImageFolder, uploadFileFolder +}; diff --git a/backend/package-lock.json b/backend/package-lock.json index 312094c..7f71afd 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcryptjs": "^3.0.2", "chalk": "^4.1.2", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^17.2.0", "express": "^5.1.0", @@ -18,7 +20,9 @@ "jsonwebtoken": "^9.0.2", "mongoose": "^8.16.3", "morgan": "^1.10.0", - "nginx": "^1.1.0" + "multer": "^2.0.2", + "nginx": "^1.1.0", + "uuid": "^11.1.0" }, "devDependencies": { "jest": "^30.0.4" @@ -1734,6 +1738,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1867,6 +1877,15 @@ "node": ">= 0.8" } }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -2007,9 +2026,19 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2277,6 +2306,21 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -2334,6 +2378,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -4363,6 +4426,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -4373,6 +4445,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mongodb": { "version": "6.17.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz", @@ -4458,16 +4542,16 @@ "license": "MIT" }, "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", - "on-headers": "~1.0.2" + "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" @@ -4523,6 +4607,67 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -4646,9 +4791,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -5016,6 +5161,20 @@ "node": ">=0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5389,6 +5548,43 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -5738,6 +5934,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", @@ -5819,6 +6021,25 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -6032,6 +6253,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/package.json b/backend/package.json index fe973ad..d06388f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "main": "index.js", "scripts": { + "coverage": "jest --coverage=true", "test": "jest", "dev": "nodemon .", "start": "node ." @@ -12,7 +13,9 @@ "license": "ISC", "description": "", "dependencies": { + "bcryptjs": "^3.0.2", "chalk": "^4.1.2", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^17.2.0", "express": "^5.1.0", @@ -21,7 +24,9 @@ "jsonwebtoken": "^9.0.2", "mongoose": "^8.16.3", "morgan": "^1.10.0", - "nginx": "^1.1.0" + "multer": "^2.0.2", + "nginx": "^1.1.0", + "uuid": "^11.1.0" }, "devDependencies": { "jest": "^30.0.4" diff --git a/backend/routes/auth.route.js b/backend/routes/auth.route.js index 373fe83..9e45e90 100644 --- a/backend/routes/auth.route.js +++ b/backend/routes/auth.route.js @@ -5,5 +5,6 @@ const router = express.Router(); router.post('/login', authControl.login); router.post('/signup', authControl.signup); +router.post('/logout', authControl.logout); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/routes/message.route.js b/backend/routes/message.route.js index 7c8def6..cc47791 100644 --- a/backend/routes/message.route.js +++ b/backend/routes/message.route.js @@ -1,13 +1,16 @@ const express = require('express'); const messageControl = require('../controllers/message.controller'); +const { verifyToken, verifyAdmin } = require('../controllers/auth.controller'); const router = express.Router(); /* NOTE: All but GET must be authenticated with admin! */ - router.post('/', messageControl.createMessage); + +router.use(verifyToken); +router.use(verifyAdmin); router.get('/', messageControl.getMessages); module.exports = router; diff --git a/backend/routes/project.route.js b/backend/routes/project.route.js index 07f3926..9cb55ef 100644 --- a/backend/routes/project.route.js +++ b/backend/routes/project.route.js @@ -1,17 +1,18 @@ const express = require('express'); const projectControl = require('../controllers/project.controller'); +const { verifyToken, verifyAdmin } = require('../controllers/auth.controller'); + const router = express.Router(); -/* - NOTE: All but GET must be authenticated with admin! -*/ +router.get('/public', projectControl.getDisplayProjects); + +router.use(verifyToken); +router.get('/', projectControl.getAllProjects); +router.get('/:id', projectControl.getProject); -router.route('/') - .get(projectControl.getAllProjects) - .post(projectControl.createProject); -router.route('/:id') - .get(projectControl.getProject) - .patch(projectControl.updateProject) - .delete(projectControl.deleteProject); +router.use(verifyAdmin); +router.delete('/:id', projectControl.deleteProject); +router.post('/', projectControl.createProject); +router.patch('/:id', projectControl.updateProject); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/routes/uploads.route.js b/backend/routes/uploads.route.js new file mode 100644 index 0000000..1388fec --- /dev/null +++ b/backend/routes/uploads.route.js @@ -0,0 +1,19 @@ +const express = require('express'); +const uuid = require('uuid').v6; + +const { verifyToken, verifyAdmin } = require('../controllers/auth.controller'); +const { uploadImageMulter, uploadFileMulter } = require('../multer/multerConfig'); +const fileLoader = require('../multer/fileUpload'); + +const router = express.Router(); + +router.get('/pdfs/:name', fileLoader.downloadFile); +router.get('/images/:name', fileLoader.downloadImage); + + +router.use(verifyToken); +router.use(verifyAdmin); +router.post('/pdfs', uploadFileMulter.single('file'), fileLoader.uploadFile); +router.post('/images', uploadImageMulter.single('image'), fileLoader.uploadImage); + +module.exports = router; diff --git a/backend/routes/user.route.js b/backend/routes/user.route.js new file mode 100644 index 0000000..6e80b97 --- /dev/null +++ b/backend/routes/user.route.js @@ -0,0 +1,19 @@ +const express = require('express'); +const userControl = require('../controllers/user.controller'); +const { verifyToken, verifyAdmin } = require('../controllers/auth.controller'); +const User = require('../models/user.model'); + +const router = express.Router(); + +router.use(verifyToken); +router.use(verifyAdmin); +router.route('/') + .get(userControl.getAllUsers) + .post(userControl.createUser); + +router.route('/:id') + .get(userControl.getUser) + .patch(userControl.updateUser) + .delete(userControl.deleteUser); + +module.exports = router; \ No newline at end of file diff --git a/backend/tests/auth.test.js b/backend/tests/auth.test.js new file mode 100644 index 0000000..f2663e2 --- /dev/null +++ b/backend/tests/auth.test.js @@ -0,0 +1,299 @@ +const User = require('../models/user.model'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const chalk = require('chalk'); +const authControls = require('../controllers/auth.controller'); +const mockUsers = require('./mockUsers'); + +jest.mock('../models/user.model'); +jest.mock('bcryptjs'); +jest.mock('jsonwebtoken'); + +beforeAll(() => { + bcrypt.compare.mockImplementation(async (pass1, pass2) => { + return pass1 === pass2; + }); +}); + +afterAll(() => { + jest.resetAllMocks(); +}); + +describe('login test', () => { + + afterEach(() => { + jest.clearAllMocks(); + }); + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + clearCookie: jest.fn(), + cookie: jest.fn(), + }; + + test('should login with name and password', async () => { + const username = mockUsers[ 0 ].username; + const password = mockUsers[ 0 ].password; + const user = { ...mockUsers[ 0 ], save: jest.fn().mockResolvedValue(true) }; + const req = { body: { data: { username, password } }, cookies: {} }; + + User.findOne.mockResolvedValue(user); + jwt.sign.mockReturnValue("token testx"); + const resp = await authControls.login(req, res); + expect(bcrypt.compare.mock.calls).toHaveLength(1); + expect(bcrypt.compare.mock.calls[ 0 ]).toStrictEqual([ password, user.password ]); + + expect(jwt.sign.mock.calls).toHaveLength(1); + expect(jwt.sign.mock.calls[ 0 ]).toStrictEqual([ { username, role: mockUsers[ 0 ].role }, process.env.JWT_SECRET, { expiresIn: '10h' } ]); + + expect(user.save).toHaveBeenCalled(); + + expect(res.clearCookie.mock.calls).toHaveLength(1); + expect(res.cookie.mock.calls).toHaveLength(1); + expect(res.cookie.mock.calls[ 0 ][ 1 ]).toBe("token testx"); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: { username, role: mockUsers[ 0 ].role } }); + }); + + test('should fail login without password or username', async () => { + const req = { body: { data: { username: "hi" } }, cookies: {} }; + + User.findOne.mockRejectedValue(null); + + const resp = await authControls.login(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing username or password!" }); + }); + + test('should fail with invalid username', async () => { + const req = { body: { data: { username: "hi", password: "0eej" } }, cookies: {} }; + + User.findOne.mockResolvedValue(null); + + const resp = await authControls.login(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(401); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Invalid credentials!" }); + }); + + test('should fail with wrong password', async () => { + const req = { body: { data: { username: mockUsers[ 0 ].username, password: "wrong" } }, cookies: {} }; + + User.findOne.mockResolvedValue(mockUsers[ 0 ].username); + + const resp = await authControls.login(req, res); + + expect(bcrypt.compare.mock.calls).toHaveLength(1); + expect(await bcrypt.compare.mock.results[ 0 ].value).toBe(false); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(401); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Invalid credentials!" }); + + }); + + test('should fail login without data object', async () => { + const req = { body: {}, cookies: { jwt_token: "" } }; + + User.findOne.mockResolvedValue(null); + + const resp = await authControls.login(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing username or password!" }); + }); + + test('should fail login with empty data', async () => { + const req = { body: { data: {} }, cookies: { jwt_token: "" } }; + + User.findOne.mockResolvedValue(null); + + const resp = await authControls.login(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing username or password!" }); + }); + + test('should login with valid jwtToken', async () => { + const req = { body: {}, cookies: { jwt_token: "tokenxx" } }; + const user = mockUsers[ 0 ]; + + User.findOne.mockResolvedValue(user); + jwt.verify.mockReturnValue({ exp: Math.ceil(Date.now() / 1000) + 10, username: user.username, role: user.role }); + + const resp = await authControls.login(req, res); + + expect(jwt.verify.mock.calls).toHaveLength(1); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: { username: user.username, role: user.role } }); + }); + + test('should login with user & pass with expired token', async () => { + const username = mockUsers[ 0 ].username; + const password = mockUsers[ 0 ].password; + const user = { ...mockUsers[ 0 ], save: jest.fn().mockResolvedValue(true) }; + const req = { body: { data: { username, password } }, cookies: { jwt_token: "invalidated" } }; + + User.findOne.mockResolvedValue(user); + jwt.sign.mockReturnValue("token testx"); + jwt.verify.mockImplementation(() => { throw new Error("Expired token"); }); + const resp = await authControls.login(req, res); + + expect(jwt.verify.mock.calls).toHaveLength(1); + + expect(res.clearCookie.mock.calls).toHaveLength(2); + expect(res.cookie.mock.calls).toHaveLength(1); + expect(res.cookie.mock.calls[ 0 ][ 1 ]).toBe("token testx"); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: { username, role: mockUsers[ 0 ].role } }); + }); + + test('should fail with invalid user & pass with expired token', async () => { + const username = mockUsers[ 0 ].username; + const password = "wrong pass"; + const user = { ...mockUsers[ 0 ], save: jest.fn().mockResolvedValue(true) }; + const req = { body: { data: { username, password } }, cookies: { jwt_token: "invalidated" } }; + + User.findOne.mockResolvedValue(user); + jwt.sign.mockReturnValue("token testx"); + jwt.verify.mockImplementation(() => { throw new Error("Expired token"); }); + const resp = await authControls.login(req, res); + + expect(jwt.verify.mock.calls).toHaveLength(1); + + expect(bcrypt.compare.mock.calls).toHaveLength(1); + expect(bcrypt.compare.mock.calls[ 0 ]).toStrictEqual([ password, user.password ]); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(401); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Invalid credentials!" }); + }); + + test('should catch the error', async () => { + const req = { body: { data: { username: mockUsers[ 0 ].username, password: mockUsers[ 0 ].password } }, cookies: {} }; + + User.findOne.mockRejectedValue(new Error("Networking error!")); + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + + const resp = await authControls.login(req, res); + + expect(consoleSpy.mock.calls).toHaveLength(1); + expect(consoleSpy.mock.calls[ 0 ][ 0 ]).toBe(chalk.red(`LOGIN ERROR: Networking error!`)); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(500); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Something went wrong. Try again." }); + }); +}); + +describe('logout test', () => { + + afterEach(() => { + jest.clearAllMocks(); + }); + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + clearCookie: jest.fn(), + cookie: jest.fn(), + }; + + test('should successfully logout', async () => { + const req = { body: {}, cookies: { jwt_token: "test tokenyyx" } }; + jwt.verify.mockReturnValue({ + username: mockUsers[ 0 ].username, + role: mockUsers[ 0 ].role, + exp: Math.ceil(Date.now() / 1000) + 100 + }); + User.findOne.mockResolvedValue(mockUsers[ 0 ]); + + const resp = await authControls.logout(req, res); + + expect(res.clearCookie.mock.calls).toHaveLength(1); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: mockUsers[ 0 ].username }); + + }); + + test('should fail to logout with invalid credentials', async () => { + const req = { body: {}, cookies: { jwt_token: "testxx" } }; + jwt.verify.mockReturnValue({ + username: "wrong", + role: "again", + exp: Math.ceil(Date.now() / 1000) + 100 + }); + + User.findOne.mockResolvedValue(null); + + const resp = await authControls.logout(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(401); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Invalid credentials." }); + }); + + test('should fail with no token', async () => { + const req = { body: {}, cookies: {} }; + jwt.verify.mockReturnValue(new Error("Invalid token!")); + + const resp = await authControls.logout(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(401); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Invalid credentials." }); + }); + + test('should fail with expired token', async () => { + const req = { body: {}, cookies: { jwt_token: "testxx" } }; + jwt.verify.mockImplementation(() => { throw new Error("Expired token"); }); + + const resp = await authControls.logout(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(401); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Invalid credentials." }); + }); +}); \ No newline at end of file diff --git a/backend/tests/message.test.js b/backend/tests/message.test.js index 0c44334..903a667 100644 --- a/backend/tests/message.test.js +++ b/backend/tests/message.test.js @@ -1,14 +1,169 @@ +const Message = require('../models/message.model'); const messageControl = require('../controllers/message.controller'); +const mockMessages = require('./mockMessages'); +const chalk = require('chalk'); -describe('Project Controllers', () => { +jest.mock("../models/message.model"); + +describe('Get all messages', () => { + + const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should test', async () => { + const req = {}; + Message.find.mockImplementation(async (filter, hideAttributes) => { + const hiddenAttributes = hideAttributes.split(" ").map(key => key.substring(1)); + const displayMessages = mockMessages.map(message => { + const modifiedMessage = { ...message }; + + hiddenAttributes.forEach(key => { + if (key in modifiedMessage) + delete modifiedMessage[ key ]; + }); + + return modifiedMessage; + }); + + return displayMessages; + }); + + const expectedMessages = mockMessages.map(message => { + const { _id, __v, ...filteredMessage } = message; + return filteredMessage; + }); + + const resp = await messageControl.getMessages(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: expectedMessages }); - beforeAll(() => { - const list = [ 1 ]; }); - test('should test', () => { - const data = 1; - expect(data + 1).toBe(2); + test('should catch error', async () => { + const req = {}; + Message.find.mockRejectedValue(new Error("something")).mockName('messageFind'); + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => { }); + + const resp = await messageControl.getMessages(req, res); + + expect(consoleSpy.mock.calls).toHaveLength(1); + expect(consoleSpy.mock.calls[ 0 ][ 0 ]).toBe(chalk.red(`MESSAGE ROUTE ERROR: something!`)); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(500); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Something went wrong. Try again." }); }); }); + +describe('Create a message', () => { + + const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should successfully create a message', async () => { + const name = "newName"; + const email = "hi@yahoo.com"; + const contents = "lorem"; + const newMessage = { _id: "4A85FBC4BA190A870C418649", __v: 0, name, email, contents }; + + const req = { body: { data: { name, email, contents } } }; + Message.create.mockResolvedValue(newMessage); + + const resp = await messageControl.createMessage(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(201); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: name }); + + }); + + test('should fail if missing body', async () => { + const req = {}; + Message.create.mockRejectedValue(new Error("Missing required fields!")); + + const resp = await messageControl.createMessage(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing required fields!" }); + }); + + test('should fail if missing data object', async () => { + const req = { body: {} }; + Message.create.mockRejectedValue(new Error("Missing required fields!")); + + const resp = await messageControl.createMessage(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing required fields!" }); + }); + + test('should fail if data is empty', async () => { + const req = { body: { data: {} } }; + Message.create.mockRejectedValue(new Error("Missing required fields!")); + + const resp = await messageControl.createMessage(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing required fields!" }); + }); + + test('should check that message was created', async () => { + const name = "newName"; + const email = "hi@yahoo.com"; + const contents = "lorem"; + + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => { }); + + const req = { body: { data: { name, email, contents } } }; + Message.create.mockRejectedValue(new Error("Something went wrong")); + + const resp = await messageControl.createMessage(req, res); + + expect(consoleSpy.mock.calls).toHaveLength(1); + expect(consoleSpy.mock.calls[ 0 ][ 0 ]).toBe(chalk.red(`MESSAGE ROUTE ERROR: Something went wrong!`)); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(500); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Something went wrong. Try again." }); + }); + + test('should fail if missing required fields', async () => { + const req = { body: { data: { name: "fake name" } } }; + Message.create.mockRejectedValue(new Error("Missing required fields!")); + + const resp = await messageControl.createMessage(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing required fields!" }); + }); +}) + diff --git a/backend/tests/mockMessages.js b/backend/tests/mockMessages.js new file mode 100644 index 0000000..e29c0de --- /dev/null +++ b/backend/tests/mockMessages.js @@ -0,0 +1,58 @@ +const mockMessages = [ + { + "_id": "DA18960E35EA77815B580036", + "name": "name1", + "email": "testing123@me", + "contents": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor." + }, + { + "_id": "76CA9E8A538FA208A172A7BB", + "name": "name again 2", + "email": "hello@you.com", + "contents": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor." + }, + { + "_id": "3931FA5E0CB14F2711E32DE8", + "name": "message 3", + "email": "official@gmail.com", + "contents": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor." + }, + { + "_id": "4A85FBC4BA190A870C418646", + "name": "Gerald", + "email": "me@yahoo.com", + "contents": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor." + }, + { + "_id": "1EB9441B9C55963181065F2C", + "name": "David", + "email": "something@outlook.com", + "contents": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor." + }, + { + "_id": "1AFBFD5797998E9ED214E1A3", + "name": "Daisy", + "email": "my@customEmail.com", + "contents": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor." + }, + { + "_id": "6C2935AB0A3BA2A1B07B2C62", + "name": "Harold", + "email": "email@gmail.com", + "contents": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor." + }, + { + "_id": "AA481D5596990FC94FBEA002", + "name": "Mary", + "email": "mailingEmail@email.com", + "contents": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor." + }, + { + "_id": "36C681D7B2615B056A630B13", + "name": "Lucas", + "email": "yahoo@yahoo.com", + "contents": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor." + }, +]; + +module.exports = mockMessages; diff --git a/backend/tests/mockUsers.js b/backend/tests/mockUsers.js new file mode 100644 index 0000000..88361d7 --- /dev/null +++ b/backend/tests/mockUsers.js @@ -0,0 +1,36 @@ +const mockUsers = [ + { + "_id": "AD2C28E2ADB48EF4B7F10163", + "username": "hi123", + "password": "xx11", + "role": "Admin", + "loggedIn": "true", + "__v": 0 + }, + { + "_id": "AD2C28E2ADB48EF4B7F10168", + "username": "hixx123", + "password": "xx112223", + "role": "Admin", + "loggedIn": "false", + "__v": 0 + }, + { + "_id": "6D7B8758EA8E0534304C5BD2", + "username": "hi again", + "password": "12345123", + "role": "Elite", + "loggedIn": "false", + "__v": 0 + }, + { + "_id": "3828272C06418043D9B5DEF2", + "username": "another username", + "password": "1dfs9sf11", + "role": "Elite", + "loggedIn": "true", + "__v": 0 + }, +]; + +module.exports = mockUsers; diff --git a/backend/tests/project.test.js b/backend/tests/project.test.js index 5ec7868..d04c3d0 100644 --- a/backend/tests/project.test.js +++ b/backend/tests/project.test.js @@ -1,33 +1,491 @@ const Project = require('../models/project.model.js'); const projectControl = require('../controllers/project.controller.js'); +const mockProjects = require('./mockProjects.js'); +const chalk = require('chalk'); -const excludeOutFields = (arr, fields) => { - const newArr = arr.map(item => { - return Object.fromEntries( - Object.entries(item).filter(key => { - return !(fields.includes(key)); - }) - ); +jest.mock('../models/project.model.js'); + +// TODO test coverage for display projects +describe('Get public projects', () => { + + const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should retrieve public projects and hide internal details', async () => { + const req = {}; + const expectedProjects = mockProjects.map(project => { + const { _id, __v, ...filteredProject } = project; + if (project.show) + return filteredProject; + }); + + Project.find.mockImplementation(async ({ show }, hideAttributes) => { + const hiddenAttributes = hideAttributes.split(" ").map(key => key.substring(1)); + + const displayProjects = mockProjects.map(project => { + const modifiedProject = { ...project }; + + hiddenAttributes.forEach(key => { + if (key in modifiedProject) + delete modifiedProject[ key ]; + }); + if (show == modifiedProject.show) + return modifiedProject; + }); + + return displayProjects; + }); + + const resp = await projectControl.getDisplayProjects(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: expectedProjects }); + }); + + test('should catch error', async () => { + const req = {}; + Project.find.mockRejectedValue(new Error("something")).mockName('projectFind'); + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => { }); + + const resp = await projectControl.getDisplayProjects(req, res); + + expect(consoleSpy.mock.calls).toHaveLength(1); + expect(consoleSpy.mock.calls[ 0 ][ 0 ]).toBe(chalk.red(`PROJECT ROUTE ERROR: something!`)); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(500); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Something went wrong. Try again." }); + }); +}); + +describe('Get all projects', () => { + + const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should succeed', async () => { + const req = {}; + Project.find.mockResolvedValue(mockProjects).mockName('projectFind'); + + const resp = await projectControl.getAllProjects(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: mockProjects }); + }); + + test('should throw error', async () => { + const req = {}; + Project.find.mockRejectedValue(new Error("something")).mockName('projectFind'); + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => { }); + + const resp = await projectControl.getAllProjects(req, res); + + expect(consoleSpy.mock.calls).toHaveLength(1); + expect(consoleSpy.mock.calls[ 0 ][ 0 ]).toBe(chalk.red(`PROJECT ROUTE ERROR: something!`)); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(500); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Something went wrong. Try again." }); + }); + +}); + +describe('Get a project', () => { + + const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return valid project', async () => { + const id = "3828272C06418043D9B5DEF2"; + const req = { params: { id } }; + const expectedProject = mockProjects.find(item => item._id == id); + Project.findById.mockResolvedValue(expectedProject); + + const resp = await projectControl.getProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: expectedProject }); + }); + + test('should not find a project', async () => { + const id = "10101"; + const req = { params: { id } }; + Project.findById.mockResolvedValue(null); + + const resp = await projectControl.getProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "No such project!" }); + }); + + test('should have no params', async () => { + const req = {}; + Project.findById.mockResolvedValue(null); + + const resp = await projectControl.getProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing id!" }); + }); + + test('should check if id is undefined', async () => { + const req = { params: {} }; + + const resp = await projectControl.getProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing id!" }); + }); + + test('should be an ID cast error', async () => { + const req = { params: { id: "stuff" } }; + + Project.findById.mockRejectedValue(new Error("unable to cast id")).mockName('projectFindById'); + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => { }); + + const resp = await projectControl.getProject(req, res); + + expect(consoleSpy.mock.calls).toHaveLength(1); + expect(consoleSpy.mock.calls[ 0 ][ 0 ]).toBe(chalk.red(`PROJECT ROUTE ERROR: unable to cast id!`)); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(500); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Something went wrong. Try again." }); + }); +}); + +describe('Create a project', () => { + + const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should successfully create project', async () => { + const newProjectData = { name: "name1", image: "img.png", description: [ "1", "2" ], link: "http://localhost", show: false }; + const req = { + body: { + data: newProjectData + } + }; + const createdProject = { ...newProjectData, "_id": "6D7B8758EA8E0534304C5BD0", "__v": 0 }; + Project.create.mockResolvedValue(createdProject); + + const resp = await projectControl.createProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(201); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: { name: createdProject.name, show: createdProject.show, id: createdProject._id } }); + }); + + test('should be required missing fields', async () => { + const newProjectData = { image: "img.png", description: [ "1", "2" ], link: "http://localhost", show: false }; + const req = { + body: { + data: newProjectData + } + }; + + Project.create.mockRejectedValue(new Error("Missing required fields!")); + + const resp = await projectControl.createProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing required fields!" }); + }); + + test('should have no req body', async () => { + const req = {}; + + Project.create.mockRejectedValue(new Error("Missing required fields!")); + + const resp = await projectControl.createProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing required fields!" }); }); - return newArr; -}; + test('should handle error', async () => { + const newProjectData = { name: "name", image: "img.png", description: [ "1", "2" ], link: "http://localhost", show: false }; + const req = { + body: { + data: newProjectData + } + }; + + Project.create.mockRejectedValue(new Error("Network error")); + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => { }); -describe('Project Controllers', () => { + const resp = await projectControl.createProject(req, res); - const mockRes = { - status: jest.fn().mockReturnThis(), - json: jest.fn().mockReturnThis(), - }; + expect(consoleSpy.mock.calls).toHaveLength(1); + expect(consoleSpy.mock.calls[ 0 ][ 0 ]).toBe(chalk.red(`PROJECT ROUTE ERROR: Network error!`)); - test('should get all projects', async () => { - const rawData = require('./mockProjects.js'); - const mockReq = {}; - const projects = await projectControl.getAllProjects(mockReq, mockRes); + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(500); - expect(projects).toStrictEqual(rawData); - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.json).toHaveBeenCalledWith(rawData); + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Something went wrong. Try again." }); }); }); + +describe('Delete a project', () => { + const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return deleted project', async () => { + const id = "1F2ECE8189710476FB8021DF"; + const req = { params: { id } }; + const expectedProject = mockProjects.find(item => item._id == id); + Project.findByIdAndDelete.mockResolvedValue(expectedProject); + + const resp = await projectControl.deleteProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: { name: expectedProject.name, show: expectedProject.show } }); + }); + + test('should not find a project to delete', async () => { + const id = "10101"; + const req = { params: { id } }; + Project.findByIdAndDelete.mockResolvedValue(null); + + const resp = await projectControl.deleteProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "No such project!" }); + }); + + test('should check if id is undefined', async () => { + const req = { params: {} }; + Project.findByIdAndDelete.mockResolvedValue(null); + + const resp = await projectControl.deleteProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing id!" }); + }); + + test('should be an ID cast error', async () => { + const req = { params: { id: "stuff" } }; + + Project.findByIdAndDelete.mockRejectedValue(new Error("unable to cast id")).mockName('projectFindByIdAndDelete'); + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => { }); + + const resp = await projectControl.deleteProject(req, res); + + expect(consoleSpy.mock.calls).toHaveLength(1); + expect(consoleSpy.mock.calls[ 0 ][ 0 ]).toBe(chalk.red(`PROJECT ROUTE ERROR: unable to cast id!`)); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(500); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Something went wrong. Try again." }); + }); +}); + +describe('Update a project', () => { + const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should successfully update project', async () => { + const req = { params: { id: "6974576B526AC6FB45272692" }, body: { data: { link: "https://www.google.com", image: "noimage.jpg", show: true } } }; + + const editedProject = { ...mockProjects.find(item => item._id == req.params.id), ...req.body.data }; + Project.findByIdAndUpdate.mockResolvedValue(editedProject); + + const resp = await projectControl.updateProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: { name: editedProject.name, show: editedProject.show } }); + }); + + test('should be missing id', async () => { + const req = { params: {}, body: { data: { link: "https://www.google.com", image: "noimage.jpg", show: true } } }; + + Project.findByIdAndUpdate.mockResolvedValue(null); + + const resp = await projectControl.updateProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Missing id!" }); + }); + + test('should keep project when no data', async () => { + // data object not present at all + const req = { params: { id: "6974576B526AC6FB45272692" }, body: {} }; + let mockProjectsCopy = mockProjects; + let newProject; + + const expectedNewProject = mockProjects.find(item => item._id == req.params.id); + Project.findByIdAndUpdate.mockImplementation(async (id, data, options) => { + const i = mockProjectsCopy.findIndex(item => item._id == id); + newProject = { ...mockProjectsCopy[ i ], ...data }; + mockProjectsCopy[ i ] = newProject; + + return (options.new) ? newProject : originalProject; + }); + + const resp = await projectControl.updateProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: { name: expectedNewProject.name, show: expectedNewProject.show } }); + + expect(newProject).toStrictEqual(expectedNewProject); + }); + + test('should keep project no body', async () => { + // data object not present at all + const req = { params: { id: "6974576B526AC6FB45272692" } }; + let mockProjectsCopy = mockProjects; + let newProject; + + const expectedNewProject = mockProjects.find(item => item._id == req.params.id); + Project.findByIdAndUpdate.mockImplementation(async (id, data, options) => { + const i = mockProjectsCopy.findIndex(item => item._id == id); + newProject = { ...mockProjectsCopy[ i ], ...data }; + mockProjectsCopy[ i ] = newProject; + + return (options.new) ? newProject : originalProject; + }); + + const resp = await projectControl.updateProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: { name: expectedNewProject.name, show: expectedNewProject.show } }); + + expect(newProject).toStrictEqual(expectedNewProject); + }); + + test('should be unable to find project', async () => { + const req = { params: { id: "6974" }, body: { data: { link: "https://www.google.com", image: "noimage.jpg", show: true } } }; + Project.findByIdAndUpdate.mockResolvedValue(null); + + const resp = await projectControl.updateProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(400); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "No such project!" }); + }); + + test('should be unable to modify id or internal version', async () => { + const req = { params: { id: "6974576B526AC6FB45272692" }, body: { data: { _id: "123", __v: 1, createdAt: "0", updatedAt: "1" } } }; + let mockProjectsCopy = mockProjects; + + const index = mockProjectsCopy.findIndex(item => item._id == req.params.id); + expect(index).not.toBe(-1); + + const originalProject = mockProjectsCopy[ index ]; + let newProject; + Project.findByIdAndUpdate.mockImplementation(async (id, data, options) => { + const i = mockProjectsCopy.findIndex(item => item._id == id); + newProject = { ...mockProjectsCopy[ i ], ...data }; + mockProjectsCopy[ i ] = newProject; + + return (options.new) ? newProject : originalProject; + }); + + const resp = await projectControl.updateProject(req, res); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(200); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "success", data: { name: originalProject.name, show: originalProject.show } }); + + expect(newProject).toStrictEqual(originalProject); + }); + + test('should handle (id) error', async () => { + const req = { params: { id: "stuff" }, body: { data: { name: "123" } } }; + + Project.findByIdAndUpdate.mockRejectedValue(new Error("unable to cast id")).mockName('projectFindByIdAndUpdate'); + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => { }); + + const resp = await projectControl.updateProject(req, res); + + expect(consoleSpy.mock.calls).toHaveLength(1); + expect(consoleSpy.mock.calls[ 0 ][ 0 ]).toBe(chalk.red(`PROJECT ROUTE ERROR: unable to cast id!`)); + + expect(resp.status.mock.calls).toHaveLength(1); + expect(resp.status.mock.calls[ 0 ][ 0 ]).toBe(500); + + expect(resp.json.mock.calls).toHaveLength(1); + expect(resp.json.mock.calls[ 0 ][ 0 ]).toStrictEqual({ status: "failed", message: "Something went wrong. Try again." }); + }); +}) + + diff --git a/compose.yaml b/compose.yaml index 057f771..fe25a2b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -6,9 +6,9 @@ services: env_file: - backend/.env restart: on-failure - expose: - - 3000 + ports: + - "3000:3000" frontend: build: frontend ports: - - "3000:5173" + - "8080:5173" diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 323a1ee..00a24fe 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -5,4 +5,4 @@ RUN npm ci COPY . . RUN npm run build EXPOSE 5173 -CMD [ "npm", "start", "--host" ] \ No newline at end of file +CMD [ "npm", "start"] \ No newline at end of file diff --git a/frontend/TODO.md b/frontend/TODO.md deleted file mode 100644 index f4da199..0000000 --- a/frontend/TODO.md +++ /dev/null @@ -1,12 +0,0 @@ -### Frontpage - -- Intro animations for other sections (intro component?) -- Reorganize info in about me in friendlier way -- Logo for favicon? - -### Add admin page for metadata - -- Able to upload resume and change website info -- Visit counter? -- Admin user with authentication (Authentication header?) -- Ability to add sudo viewer for recruiters diff --git a/frontend/old/data.js b/frontend/old/data.js deleted file mode 100644 index 154edea..0000000 --- a/frontend/old/data.js +++ /dev/null @@ -1,20 +0,0 @@ -let oldProjects = [ - { - name: "Clubfinity", - image: "clubfinity.png", - description: [ - "A mobile app for university students to get all the latest information about their favorite clubs. Clubfinity gives students the opportunity to discover and experience new clubs, as well as a way to be continually notified of upcoming events!", - "The app is developed on a MERN tech stack, with the frontend developed using React Native, the backend with NodeJS with the ExpressJS framework, and MongoDB NoSQL database. Frontend and backend are connected through a stateless JSON REST API. Code version control managed by Git and Gitlab, with a CI/CD pipeline for automated testing upon merging, improving code quality by 50%. Collaboration is done in teams of 3-4 with sprint meetings every 2 weeks to discuss app progress and new sprint backlog items.", - ], - link: "https://gitlab.com/ufsec/clubfinity", - }, - { - name: "Jonra", - image: "jonra.png", - description: [ - "A website for developers and project managers to manage tasks and boards for projects. Students are also able to use Jonra to manage homework and other tasks for classes or daily life!", - "The frontend is developed through React for user interaction. The backend is built off of the Django framework with a SQLite database managed with Django's ORM for CRUD operations. Frontend and backend communicate with a stateless JSON REST API along with user authentication. Code version control is handled by Git and Github. Collaboration was done with 2 others utilizing Agile methology with regular scrum meetings and sprint meetings every 3-4 weeks.", - ], - link: "https://github.com/JonKissil/Jonra", - }, -]; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 44f0cfb..af755f6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "axios": "^1.10.0", + "jsonwebtoken": "^9.0.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3" @@ -18,11 +19,13 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", - "vite": "^7.0.3" + "vite": "^7.0.3", + "vitest": "^3.2.4" } }, "node_modules/@ampproject/remapping": { @@ -321,6 +324,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", @@ -996,6 +1009,34 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", @@ -1035,6 +1076,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.19", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", @@ -1367,6 +1419,23 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1422,6 +1491,155 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1462,6 +1680,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1485,6 +1716,35 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", + "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1553,6 +1813,22 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1597,6 +1873,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1614,6 +1907,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1709,6 +2012,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1739,6 +2052,22 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.180", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.180.tgz", @@ -1746,6 +2075,13 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1764,6 +2100,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2024,6 +2367,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2034,6 +2387,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2141,6 +2504,23 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -2228,6 +2608,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2241,6 +2642,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", @@ -2315,6 +2742,13 @@ "node": ">= 0.4" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2362,6 +2796,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2382,33 +2826,103 @@ "dev": true, "license": "ISC" }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, - "license": "MIT" + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "argparse": "^2.0.1" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=10" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, - "license": "MIT", - "bin": { + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { "jsesc": "bin/jsesc" }, "engines": { @@ -2449,6 +2963,61 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2489,6 +3058,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2496,6 +3101,19 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2506,6 +3124,57 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2549,11 +3218,20 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2639,6 +3317,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2672,6 +3357,47 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2866,6 +3592,26 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -2911,6 +3657,26 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2921,6 +3687,124 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2934,6 +3818,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2947,6 +3851,61 @@ "node": ">=8" } }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -2964,6 +3923,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3093,6 +4082,102 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3109,6 +4194,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -3119,6 +4221,101 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6c45648..30de5b4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,9 @@ "type": "module", "main": "index.html", "scripts": { - "dev": "vite . --host", + "test": "vitest", + "coverage": "vitest run --coverage", + "dev": "vite .", "build": "vite build", "start": "vite dist/. --host", "lint": "eslint .", @@ -13,6 +15,7 @@ }, "dependencies": { "axios": "^1.10.0", + "jsonwebtoken": "^9.0.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3" @@ -22,10 +25,12 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", - "vite": "^7.0.3" + "vite": "^7.0.3", + "vitest": "^3.2.4" } } diff --git a/frontend/public/images/delete.jpg b/frontend/public/images/delete.jpg new file mode 100644 index 0000000..219d0c8 Binary files /dev/null and b/frontend/public/images/delete.jpg differ diff --git a/frontend/public/images/delete.png b/frontend/public/images/delete.png new file mode 100644 index 0000000..d7b4484 Binary files /dev/null and b/frontend/public/images/delete.png differ diff --git a/frontend/public/images/noimage.jpg b/frontend/public/images/noimage.jpg new file mode 100644 index 0000000..0ca3bbd Binary files /dev/null and b/frontend/public/images/noimage.jpg differ diff --git a/frontend/public/images/plus.jpg b/frontend/public/images/plus.jpg new file mode 100644 index 0000000..01962c4 Binary files /dev/null and b/frontend/public/images/plus.jpg differ diff --git a/frontend/src/App.css b/frontend/src/App.css index 5fe536c..2a200a9 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -35,3 +35,14 @@ h1 { font-size: 2.5rem; } +@media (width <= 550px) { + html { + font-size: 12px; + } +} + +@media (width <= 375px) { + html { + font-size: 10px; + } +} \ No newline at end of file diff --git a/frontend/src/Pages/Admin.css b/frontend/src/Pages/Admin.css index 3006661..0957d35 100644 --- a/frontend/src/Pages/Admin.css +++ b/frontend/src/Pages/Admin.css @@ -2,4 +2,32 @@ text-align: center; padding: 1rem; font-size: 4rem; +} + +#logout { + display: flex; + gap: 1rem; + flex-flow: column wrap; + justify-content: center; + align-items: center; +} + +.logout-button { + margin-top: -1rem; + padding: 1rem; + font-size: 2rem; + border: 1px solid var(--complement-color); + background-color: var(--complement-color); + color: var(--text-color); + border-radius: 25px; + + transform: scale(1); + transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; +} + +.logout-button:hover { + cursor: pointer; + box-shadow: 0 0 20px rgba(252, 79, 79, 0.5); + transform: scale(1.05); + transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; } \ No newline at end of file diff --git a/frontend/src/Pages/Admin.jsx b/frontend/src/Pages/Admin.jsx index ac98097..08aabbd 100644 --- a/frontend/src/Pages/Admin.jsx +++ b/frontend/src/Pages/Admin.jsx @@ -1,14 +1,73 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import AdminHeader from "../adminComponents/AdminHeader"; import AdminProjectListing from "../adminComponents/AdminProjectListing/AdminProjectListing"; import MessageList from "../adminComponents/MessageList/MessageList"; +import axios from "axios"; import "./Admin.css"; function Admin() { + axios.defaults.withCredentials = true; + const [errorMessage, setErrorMessage] = useState(""); + const [userObj, setUserObj] = useState({ username: "", role: "" }); + + const navigate = useNavigate(); + + useEffect(() => { + const autoLogin = async () => { + try { + const resp = await axios.post( + "http://localhost:3000/api/auth/login", + {} + ); + if (resp.data?.status === "success") { + const { username, role } = resp.data.data; + setUserObj({ username, role }); + } + } catch (err) { + console.error(err.message); + + if (err.response?.status <= 403 && err.response.status >= 400) + navigate("/admin"); + } + }; + + autoLogin(); + }, []); + + const handleLogout = async () => { + try { + const resp = await axios.post( + "http://localhost:3000/api/auth/logout", + {} + ); + + if (resp.data.status === "success") { + navigate("/#"); + } + } catch (err) { + setErrorMessage( + err.response?.data?.message || "Failed to logout! Try again." + ); + setTimeout(() => { + setErrorMessage(""); + }, 3000); + } + }; + return ( - +
- + +
+ +

+ {errorMessage} +

+
); } diff --git a/frontend/src/Pages/Main.jsx b/frontend/src/Pages/Main.jsx index 6dca3d9..4cb42ea 100644 --- a/frontend/src/Pages/Main.jsx +++ b/frontend/src/Pages/Main.jsx @@ -1,4 +1,3 @@ -// import Layout from "../components/Layout/Layout"; import EmergeView from "../components/EmergeView"; import Home from "../components/Home/Home"; import About from "../components/About/About"; @@ -11,15 +10,12 @@ function Main() { -
-
-
diff --git a/frontend/src/adminComponents/AdminEditProject/AdminEditProject.css b/frontend/src/adminComponents/AdminEditProject/AdminEditProject.css new file mode 100644 index 0000000..c7bb13a --- /dev/null +++ b/frontend/src/adminComponents/AdminEditProject/AdminEditProject.css @@ -0,0 +1,50 @@ + + +.edit-container form { + display: flex; + flex-flow: column nowrap; + gap: 2rem; +} + +.edit-item { + display: flex; + flex-flow: row nowrap; + gap: 1rem; + justify-content: flex-start; + align-items: center; + padding-bottom: 0; + margin-bottom: 0; +} + +.edit-item input { + margin: 0; + max-width: max-content; +} + +.textarea { + flex-flow: column nowrap; + align-items: flex-start; +} + +.textarea textarea { + margin-bottom: 0; +} + +.radio { + display: flex; + flex-flow: row nowrap; + justify-content: center; + padding: 0; +} + +.radio input { + margin-left: 5px; +} + +.edit-container button { + min-width: 150px; + width: 33%; + align-self: center; + background-color: rgb(26, 174, 68, 0.3); + border-color: rgb(26, 174, 68, 1); +} \ No newline at end of file diff --git a/frontend/src/adminComponents/AdminEditProject/AdminEditProject.jsx b/frontend/src/adminComponents/AdminEditProject/AdminEditProject.jsx new file mode 100644 index 0000000..2cbd93e --- /dev/null +++ b/frontend/src/adminComponents/AdminEditProject/AdminEditProject.jsx @@ -0,0 +1,134 @@ +import { useState } from "react"; +import "./AdminEditProject.css"; + +function AdminEditProject({ projectObj, saveProject }) { + const [editInfo, setEditInfo] = useState(projectObj); + const [imageFile, setImageFile] = useState(null); + + const createProject = (e) => { + e.preventDefault(); + console.log("Pressed submit!"); + + if (!editInfo?.name || !editInfo.description || !editInfo.link) { + return; + } + + saveProject({ ...editInfo, file: imageFile }); + setEditInfo({ show: false }); + }; + + const handleShowToggle = (e) => { + const show = e.target.value == "true" ? true : false; + setEditInfo({ ...editInfo, show }); + }; + + const handleFileChange = (e) => { + const files = e.target.files; + + setEditInfo({ ...editInfo, image: e.target.value }); + setImageFile(files.length ? files[0] : null); + + const reader = new FileReader(); + reader.onload = () => { + console.log(reader.result); + }; + + reader.readAsArrayBuffer(files[0]); + }; + + return ( +
+

New Project

+
+
+ + + setEditInfo({ ...editInfo, name: e.target.value }) + } + required + /> +
+ +
+ + +
+ +
+ + + setEditInfo({ ...editInfo, link: e.target.value }) + } + /> +
+ +
+ +
-
+

{submitMessageObj.message}

diff --git a/frontend/src/components/Home/Home.css b/frontend/src/components/Home/Home.css index dac43ea..ecffd0a 100644 --- a/frontend/src/components/Home/Home.css +++ b/frontend/src/components/Home/Home.css @@ -2,7 +2,7 @@ background: none; border: none; width: 100%; - min-height: 80vh; + min-height: 100vh; border-radius: 0; opacity: 0; @@ -24,7 +24,7 @@ } #home h1 { - font-size: 3.5rem; + font-size: 3rem; margin-bottom: 2rem; } @@ -41,7 +41,7 @@ } .big-description { - font-size: 2.5rem; + font-size: 2.25rem; } .highlight { @@ -51,8 +51,11 @@ .link-container { display: flex; + margin-top: 3rem; + flex-flow: row wrap; align-items: center; justify-content: flex-start; + align-content: center; gap: 1rem; } @@ -63,7 +66,6 @@ gap: 1rem; max-width: 225px; padding: 1rem; - margin: 3rem 0; text-decoration: none; background-color: var(--complement-color); border-radius: 50px; diff --git a/frontend/src/components/Listing/Listing.css b/frontend/src/components/Listing/Listing.css index 824c4fc..e4f53a4 100644 --- a/frontend/src/components/Listing/Listing.css +++ b/frontend/src/components/Listing/Listing.css @@ -22,7 +22,7 @@ text-decoration: none; color: inherit; display: flex; - align-items: center; + align-self: flex; } .listingItem img { @@ -36,7 +36,7 @@ .listingItem p { text-align: left; display: inline; - align-self: flex-end; + align-self: center; } .listingItem:hover { diff --git a/frontend/src/components/Listing/TechListing.jsx b/frontend/src/components/Listing/TechListing.jsx index 6fe56ce..5598e9d 100644 --- a/frontend/src/components/Listing/TechListing.jsx +++ b/frontend/src/components/Listing/TechListing.jsx @@ -1,58 +1,58 @@ -import ListingItem from './ListingItem'; +import ListingItem from "./ListingItem"; function TechListing() { - return ( -
-

Developer Techs / Skills

-
- - - - - - - - - -
-
- ); + return ( +
+

Developer Techs & Skills

+
+ + + + + + + + + +
+
+ ); } -export default TechListing; \ No newline at end of file +export default TechListing; diff --git a/frontend/src/components/Navigation/Navbar.css b/frontend/src/components/Navigation/Navbar.css index 950a1be..5628086 100644 --- a/frontend/src/components/Navigation/Navbar.css +++ b/frontend/src/components/Navigation/Navbar.css @@ -1,10 +1,10 @@ nav { - height: 100px; + padding: 2rem; display: flex; gap: 2rem; align-items: center; justify-content: center; - padding-inline: 2rem; + /* padding-inline: 2rem; */ animation: emerge 1s ease-out 0s 1 normal forwards; } @@ -18,20 +18,22 @@ nav { text-decoration: none; } -.nav-item a img { - width: 35px; - height: 35px; - vertical-align: middle; - padding: 2px; - border-radius: 50%; -} - -.nav-item a img:hover { - filter: brightness(0.5); -} - .nav-item a:hover { color: var(--nav-hover-color); text-decoration: underline; text-shadow: 0 0 2px var(--nav-hover-color); +} + +@media (width <= 550px) { + nav { + gap: 1.5rem; + padding-inline: 1rem; + } +} + +@media (width <= 375px) { + nav { + gap: 1rem; + padding-inline: 1rem; + } } \ No newline at end of file diff --git a/frontend/src/components/Navigation/Navbar.jsx b/frontend/src/components/Navigation/Navbar.jsx index 95514a8..1e419b2 100644 --- a/frontend/src/components/Navigation/Navbar.jsx +++ b/frontend/src/components/Navigation/Navbar.jsx @@ -2,6 +2,8 @@ import "./Navbar.css"; // TODO: fix position of LinkendIn icon to end function Navbar() { + // console.log(window.innerWidth); + return ( ); } diff --git a/frontend/src/components/ProjectListing/Project.css b/frontend/src/components/ProjectListing/Project.css index 30fd830..38a6b9c 100644 --- a/frontend/src/components/ProjectListing/Project.css +++ b/frontend/src/components/ProjectListing/Project.css @@ -12,12 +12,23 @@ } .project img { - max-height: 300px; - float: left; - margin: 0 2rem 1rem 0; + max-width: 250px; + height: auto; + display: block; + margin-inline: auto; + margin-bottom: 2rem; border-radius: 20px; } +.project ul { + display: flex; + flex-flow: column nowrap; + gap: 1rem; +} +.project li { + margin-left: 1rem; +} + .project ul, .project a { flex-grow: 2; font-size: 1.25rem; @@ -26,7 +37,7 @@ .project a { display: block; text-align: center; - margin-top: 1rem; + margin-top: 2rem; text-decoration: underline; color: var(--text-color); @@ -36,12 +47,16 @@ color: var(--nav-hover-color); } -.del-project-button { - float: right; - margin-left: -50px; - background-color: var(--complement-color); +@media (width <= 550px) { + .project img { + max-width: 200px; + height: auto; + } } -.del-project-button:hover { - background-color: red; -} +@media (width <= 375px) { + .project img { + max-width: 150px; + height: auto; + } +} \ No newline at end of file diff --git a/frontend/src/components/ProjectListing/Project.jsx b/frontend/src/components/ProjectListing/Project.jsx index 317a54d..698aa05 100644 --- a/frontend/src/components/ProjectListing/Project.jsx +++ b/frontend/src/components/ProjectListing/Project.jsx @@ -1,25 +1,40 @@ +// import { useEffect, useState } from "react"; +// import axios from "axios"; import "./Project.css"; -function Project({ name, image, description, link, canEdit, deleteProject }) { +function Project({ name, image, description, link }) { + // const [url, setUrl] = useState("/images/noimage.jpg"); + + // useEffect(() => { + // const createUrl = async () => { + // const name = `${image[0]}-${image[1]}`; + // try { + // const resp = await axios.get( + // `http://localhost:3000/api/uploads/images/${name}`, + // { timeout: 5000 } + // ); + + // setUrl(resp.data.data); + // } catch (err) { + // console.error(err.message); + // } + // }; + + // createUrl(); + // }, []); + + // console.log(`Image url: ${url}`); + return (
-

- {name} - {canEdit && ( - - )} -

+

{name}

{name}
    {description && diff --git a/frontend/src/components/ProjectListing/ProjectListing.css b/frontend/src/components/ProjectListing/ProjectListing.css index 7aa3e68..6bdebc3 100644 --- a/frontend/src/components/ProjectListing/ProjectListing.css +++ b/frontend/src/components/ProjectListing/ProjectListing.css @@ -57,19 +57,3 @@ .show-button:hover { background-color: var(--complement-color); } - -.edit-button { - padding: 5px; - border-radius: 25px; - color: var(--text-color); - width: 50px; - font-size: 1.5rem; -} - -.add-project-button { - background-color: rgb(15, 74, 1); -} - -.add-project-button:hover { - background-color: rgb(35, 173, 1); -} \ No newline at end of file diff --git a/frontend/src/components/ProjectListing/ProjectListing.jsx b/frontend/src/components/ProjectListing/ProjectListing.jsx index fc33986..72631b6 100644 --- a/frontend/src/components/ProjectListing/ProjectListing.jsx +++ b/frontend/src/components/ProjectListing/ProjectListing.jsx @@ -6,25 +6,25 @@ import "./ProjectListing.css"; // NOTE: remove console.log statements -function ProjectListing({ canEdit }) { +function ProjectListing() { const [isLoading, setIsLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(""); const [projects, setProjects] = useState([]); const [projectIndex, setProjectIndex] = useState(0); + // const [imageUrls, setImageUrls] = useState([]); useEffect(() => { const handleGetProjects = async () => { try { const result = await axios.get( - "http://localhost:3000/api/projects", + "http://localhost:3000/api/projects/public", { - filter: { show: true }, timeout: 5000, } ); - setProjects(result.data); - setIsLoading(false); + setProjects(result.data.data); + // return result.data.data; } catch (err) { console.error(`ERROR: ${err.message}`); setErrorMessage("Could not get project info."); @@ -40,18 +40,6 @@ function ProjectListing({ canEdit }) { setProjectIndex(newIndex); }; - const saveProject = async () => { - try { - // const res = await axios.post - } catch (err) { - console.error(`Save project error: ${err.message}`); - } - }; - - const deleteProject = () => { - console.log("Remove project!"); - }; - const selectedProject = projects.length ? projects[projectIndex] : {}; return ( @@ -93,11 +81,7 @@ function ProjectListing({ canEdit }) { ))}
- + )}
diff --git a/frontend/src/index.css b/frontend/src/index.css index 85485f4..fb1e7f3 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -10,10 +10,10 @@ } :root { - --text-color: whitesmoke; - --section-bg-color: rgba(36, 35, 35, 0.65); + --text-color: #f5f5f5; + --section-bg-color: #242323a6; --nav-hover-color: #94C7B6; - --complement-color: rgb(252, 79, 79); + --complement-color: #fc4f4f; } html { diff --git a/package-lock.json b/package-lock.json index 27d9932..45a49a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,149 @@ "license": "ISC", "dependencies": { "concurrently": "^9.2.0" + }, + "devDependencies": { + "cypress": "^14.5.2" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.8.tgz", + "integrity": "sha512-h0NFgh1mJmm1nr4jCwkGHwKneVYKghUyWe6TMNrk0B9zsjAJxpg8C4/+BAcmLgCPa1vj1V8rNUaILl+zYRUWBQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.14.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sizzle": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", + "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-regex": { @@ -36,6 +179,226 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -64,6 +427,88 @@ "node": ">=8" } }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", + "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "colors": "1.4.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -78,96 +523,1264 @@ "node": ">=12" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concurrently": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cypress": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.2.tgz", + "integrity": "sha512-O4E4CEBqDHLDrJD/dfStHPcM+8qFgVVZ89Li7xDU0yL/JxO/V0PEcfF2I8aGa7uA2MGNLkNUAnghPM83UcHOJw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cypress/request": "^3.0.8", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "ci-info": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-table3": "0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "hasha": "5.2.2", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.7.1", + "supports-color": "^8.1.1", + "tmp": "~0.2.3", + "tree-kill": "1.2.2", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "> 0.8" + } + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true, "license": "MIT" }, - "node_modules/concurrently": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", - "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" + "aggregate-error": "^3.0.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, "license": "MIT" }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.6.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, "engines": { - "node": ">=8" + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } }, "node_modules/require-directory": { "version": "2.1.1", @@ -178,6 +1791,27 @@ "node": ">=0.10.0" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -187,6 +1821,70 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", @@ -199,6 +1897,130 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -225,6 +2047,16 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -240,6 +2072,66 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -255,6 +2147,105 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -272,6 +2263,13 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -307,6 +2305,17 @@ "engines": { "node": ">=12" } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } } } diff --git a/package.json b/package.json index f32728e..bb3798b 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "concurrently \"cd backend && npm test\"", "front": "cd frontend && npm run dev", "back": "cd backend && npm run dev", + "build": "cd frontend && npm run build", "dev": "concurrently \"cd backend && npm run dev\" \"cd frontend && npm run dev\"", "start": "concurrently \"cd backend && npm start\" \"cd frontend && npm start\"" }, @@ -15,5 +16,8 @@ "license": "ISC", "dependencies": { "concurrently": "^9.2.0" + }, + "devDependencies": { + "cypress": "^14.5.2" } }