diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 87ab406..ec3dd98 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -80,6 +80,14 @@ jobs: push: true tags: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/linux-odyssey:app + - name: Build analytics + uses: docker/build-push-action@v2 + with: + context: . + file: ./packages/analytics/Dockerfile + push: true + tags: docker.io/${{ secrets.DOCKERHUB_USERNAME }}/linux-odyssey:analytics + - name: Build base image uses: docker/build-push-action@v2 with: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 1883512..8eacdc4 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -25,6 +25,21 @@ services: environment: - MONGO_URL=mongodb://user:user-password@db:27017/odyssey-test - DOCKER_NETWORK=linux-odyssey-players + + analytics: + image: lancatlin/linux-odyssey:analytics + build: + context: . + dockerfile: packages/analytics/Dockerfile + depends_on: + - db + networks: + - internal + ports: + - '3001:3001' + environment: + - MONGO_URL=mongodb://user:user-password@db:27017/odyssey-test + db: image: mongo restart: always diff --git a/docker-compose.yml b/docker-compose.yml index 30253cf..a6483c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,23 @@ services: - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID} - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET} + + analytics: + build: + context: . + dockerfile: packages/analytics/Dockerfile.dev + depends_on: + - db + networks: + - internal + ports: + - '3001:3001' + environment: + - MONGO_URL=mongodb://db:27017/odyssey-test + volumes: + - ./packages/analytics:/app/packages/analytics + - ./packages/models:/app/packages/models + db: image: mongo networks: diff --git a/package.json b/package.json index 6673de9..cb6fbb1 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "format": "prettier --ignore-path .prettierignore --write app server", "test": "yarn node --experimental-vm-modules $(yarn bin jest)", "test:e2e": "yarn workspace tests test", + "analytics": "yarn workspace analytics dev", "clean": "yarn workspace server clean" }, "devDependencies": { diff --git a/packages/analytics/Dockerfile b/packages/analytics/Dockerfile new file mode 100644 index 0000000..69f71f4 --- /dev/null +++ b/packages/analytics/Dockerfile @@ -0,0 +1,19 @@ +FROM node + +WORKDIR /app +COPY ./package.json yarn.lock ./ +COPY packages/analytics/package.json ./packages/analytics/package.json +COPY packages/models/package.json ./packages/models/package.json +RUN yarn install --frozen-lockfile --production + +COPY packages/analytics ./packages/analytics +COPY packages/models ./packages/models + +WORKDIR /app/packages/analytics + +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +ENV PORT=3001 +EXPOSE 3001 + +CMD [ "yarn", "start" ] \ No newline at end of file diff --git a/packages/analytics/Dockerfile.dev b/packages/analytics/Dockerfile.dev new file mode 100644 index 0000000..1262872 --- /dev/null +++ b/packages/analytics/Dockerfile.dev @@ -0,0 +1,16 @@ +FROM node + +WORKDIR /app +COPY ./package.json yarn.lock ./ +COPY packages/analytics/package.json ./packages/analytics/package.json +COPY packages/models/package.json ./packages/models/package.json +RUN yarn install --frozen-lockfile + +WORKDIR /app/packages/analytics + +ENV NODE_ENV=development +ENV HOST=0.0.0.0 +ENV PORT=3001 +EXPOSE 3001 + +CMD [ "yarn", "dev" ] diff --git a/packages/analytics/README.md b/packages/analytics/README.md new file mode 100644 index 0000000..01371e4 --- /dev/null +++ b/packages/analytics/README.md @@ -0,0 +1,59 @@ +# Analytics + +## Information provided + +### 1. **Dashboard Overview**: + +- **Total Users**: Display the total number of registered users. +- **Active Users**: Count of users who've been active in the past day, week, or month. +- **Completed Quests**: Total quests or challenges completed by all users. +- **Average Time Usage**: Average time a user spends on the platform or on challenges. + +### 2. **User Analytics**: + +- **User Growth**: A graph showing user registrations over time (daily, weekly, monthly). +- **Retention Rate**: Percentage of users who return after their first visit. +- **User Segmentation**: Group users based on criteria like their progress level, frequency of usage, or other custom metrics. + +### 3. **Quest/Challenge Analytics**: + +- **Quest Popularity**: Rank quests by the number of completions or engagements. +- **Average Completion Time**: The average time taken by users to complete each quest. +- **Drop-Off Points**: Identify quests where users abandon or fail frequently. + +### 4. **Error Analysis**: + +- **Common Errors**: List frequently mistyped or erroneous commands. +- **Quest-specific Errors**: Identify challenges where users make the most mistakes. + +### 5. **Feedback & Survey Results**: + +- **Feedback Overview**: Display recent feedback or suggestions. +- **Survey Analysis**: If you've integrated a survey, show aggregated results or key insights. + +### 6. **System Health & Usage**: + +- **Server Load**: Monitor server performance metrics like CPU usage, memory usage, etc. +- **Error Logs**: View any system errors, glitches, or issues. + +### 7. **User Details**: + +- **Search & View**: Ability to search for specific users and view their profile, progress, and activity logs. +- **User Behavior**: For selected users, see a detailed path of their activity, including the quests they've engaged with, time spent, errors made, etc. + +### 8. **Notifications & Alerts**: + +- **System Alerts**: Notifications about server downtimes, high error rates, or other critical issues. +- **User Activity Alerts**: Set alerts for specific user activities, e.g., when a user completes all challenges. + +### 9. **Engagement Metrics**: + +- **Daily Active Users**: Users who engage with the platform daily. +- **Top Users**: Identify power users or those with high engagement rates. + +### 10. **Export & Reports**: + +- **Data Export**: Option to export user data, analytics, or other metrics. +- **Generate Reports**: Create detailed reports based on selected metrics, useful for team meetings or strategy planning. + +When designing this admin dashboard, focus on clarity and ease of use. Utilize visual aids like graphs, charts, and heatmaps to make data interpretation straightforward. Since your backend is built with NodeJS and ExpressJS and you use MongoDB for data storage, you can utilize various NPM packages to aid in data visualization and dashboard creation. Ensure the dashboard is secure and accessible only to authorized personnel. diff --git a/packages/analytics/nodemon.json b/packages/analytics/nodemon.json new file mode 100644 index 0000000..d98dcc2 --- /dev/null +++ b/packages/analytics/nodemon.json @@ -0,0 +1,3 @@ +{ + "ext": "js,handlebars" +} diff --git a/packages/analytics/package.json b/packages/analytics/package.json new file mode 100644 index 0000000..53eda5a --- /dev/null +++ b/packages/analytics/package.json @@ -0,0 +1,20 @@ +{ + "name": "analytics", + "version": "1.0.0", + "main": "src/index.js", + "private": true, + "type": "module", + "license": "GPL", + "scripts": { + "start": "node src/index.js", + "dev": "nodemon src/index.js" + }, + "dependencies": { + "@linux-odyssey/models": "*", + "express": "^4.18.2", + "express-handlebars": "^7.1.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/packages/analytics/public/css/style.css b/packages/analytics/public/css/style.css new file mode 100644 index 0000000..d973b00 --- /dev/null +++ b/packages/analytics/public/css/style.css @@ -0,0 +1,19 @@ +body { + font-family: 'sans'; +} + +table, +tr, +td, +th { + border-collapse: collapse; + border-spacing: 0; + width: 100%; + border: 2px solid black; +} + +th, +td { + width: 1px; + white-space: nowrap; +} diff --git a/packages/analytics/src/config.js b/packages/analytics/src/config.js new file mode 100644 index 0000000..00e1afd --- /dev/null +++ b/packages/analytics/src/config.js @@ -0,0 +1,9 @@ +const config = { + host: process.env.HOST || 'localhost', + port: process.env.PORT || 3001, + db: process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test', +} + +config.baseUrl = process.env.BASE_URL || `http://${config.host}:${config.port}` + +export default config diff --git a/packages/analytics/src/controllers/commands.js b/packages/analytics/src/controllers/commands.js new file mode 100644 index 0000000..4692652 --- /dev/null +++ b/packages/analytics/src/controllers/commands.js @@ -0,0 +1,9 @@ +import { errorCommands } from '../models/commands.js' + +// eslint-disable-next-line import/prefer-default-export +export async function commandListController(req, res) { + const payload = { + errorCommands: await errorCommands(), + } + res.render('commands', payload) +} diff --git a/packages/analytics/src/controllers/index.js b/packages/analytics/src/controllers/index.js new file mode 100644 index 0000000..d5a6967 --- /dev/null +++ b/packages/analytics/src/controllers/index.js @@ -0,0 +1,15 @@ +import { userCount } from '../models/users.js' +import { sessionCount, sessionCompleted } from '../models/sessions.js' + +// eslint-disable-next-line import/prefer-default-export +export async function homeController(req, res) { + const payload = { + title: 'Linux Odyssey Admin', + userCount: await userCount(), + sessionCount: await sessionCount(), + sessionCompleted: await sessionCompleted(), + } + payload.completedRate = + Math.round((payload.sessionCompleted / payload.sessionCount) * 10000) / 100 + res.render('home', payload) +} diff --git a/packages/analytics/src/controllers/sessions.js b/packages/analytics/src/controllers/sessions.js new file mode 100644 index 0000000..47dc3bb --- /dev/null +++ b/packages/analytics/src/controllers/sessions.js @@ -0,0 +1,12 @@ +import { sessionList, sessionDetail } from '../models/sessions.js' + +export async function sessionListController(req, res) { + const sessions = await sessionList() + res.render('sessions', { sessions }) +} + +export async function sessionDetailController(req, res) { + const { id } = req.params + const session = await sessionDetail(id) + res.render('sessionDetail', { session }) +} diff --git a/packages/analytics/src/controllers/users.js b/packages/analytics/src/controllers/users.js new file mode 100644 index 0000000..5eb16b9 --- /dev/null +++ b/packages/analytics/src/controllers/users.js @@ -0,0 +1,6 @@ +import { userList } from '../models/users.js' + +// eslint-disable-next-line import/prefer-default-export +export async function userListController(req, res) { + res.render('users', { users: await userList() }) +} diff --git a/packages/analytics/src/index.js b/packages/analytics/src/index.js new file mode 100644 index 0000000..6977ab0 --- /dev/null +++ b/packages/analytics/src/index.js @@ -0,0 +1,24 @@ +import connectDB from '@linux-odyssey/models' +import express from 'express' +import { engine } from 'express-handlebars' +import config from './config.js' +import router from './router.js' + +async function main() { + await connectDB(config.db) + const app = express() + + // Setting up Handlebars as templating engine + app.engine('handlebars', engine()) + app.set('view engine', 'handlebars') + app.set('views', './views') + + app.use(router) + app.use(express.static('./public')) + + app.listen(config.port, config.host, () => { + console.log(`Server started on ${config.baseUrl}`) + }) +} + +main().catch(console.error) diff --git a/packages/analytics/src/models/commands.js b/packages/analytics/src/models/commands.js new file mode 100644 index 0000000..48ece73 --- /dev/null +++ b/packages/analytics/src/models/commands.js @@ -0,0 +1,28 @@ +import { Command } from '@linux-odyssey/models' + +// eslint-disable-next-line import/prefer-default-export +export async function errorCommands() { + const commands = await Command.find({ + $and: [ + { error: { $exists: true } }, + { error: { $ne: null } }, + { error: { $ne: '' } }, + ], + }) + .populate({ + path: 'session', + populate: { path: 'user' }, + }) + .exec() + return commands.map(({ command, error, createdAt, session }) => { + const { user, quest } = session + return { + command: command.slice(0, 20), + error: error.slice(0, 50), + createdAt: createdAt?.toLocaleString(), + user: user?.username, + quest, + session: session._id, + } + }) +} diff --git a/packages/analytics/src/models/sessions.js b/packages/analytics/src/models/sessions.js new file mode 100644 index 0000000..e290a8b --- /dev/null +++ b/packages/analytics/src/models/sessions.js @@ -0,0 +1,91 @@ +import { Command, Session } from '@linux-odyssey/models' + +export function sessionCount() { + return Session.find().count() +} + +export function sessionCompleted() { + return Session.find({ status: 'finished' }).count() +} + +export function averageTimeUsage() { + return Session.aggregate([ + { + $group: { + _id: '$quest', + average: { + $avg: { + $subtract: ['$finishedAt', '$createdAt'], + }, + }, + }, + }, + ]) +} + +function formatTime(time) { + const seconds = Math.floor(time / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + return `${hours}h ${minutes % 60}m ${seconds % 60}s` +} + +export async function sessionList() { + const sessions = await Session.aggregate([ + { + $lookup: { + from: 'users', + localField: 'user', + foreignField: '_id', + as: 'user', + }, + }, + { $unwind: '$user' }, + { + $lookup: { + from: 'commands', + localField: '_id', + foreignField: 'session', + as: 'commands', + }, + }, + ]) + return sessions.map( + ({ _id, user, quest, status, createdAt, finishedAt, commands }) => { + return { + _id, + user: user.username, + quest, + status, + createdAt: createdAt?.toLocaleString(), + finishedAt: finishedAt?.toLocaleString(), + usedTime: finishedAt ? formatTime(finishedAt - createdAt) : '', + commandCount: commands?.length, + } + } + ) +} + +export async function sessionDetail(id) { + const session = await Session.findById(id).populate('user').exec() + const commands = (await Command.find({ session: id })).map( + ({ command, pwd, output, error, createdAt }) => ({ + command: command?.slice(0, 20), + pwd, + output: output?.slice(0, 50), + error: error?.slice(0, 50), + createdAt: createdAt?.toLocaleString(), + }) + ) + const { _id, user, quest, status, createdAt, finishedAt } = session + return { + _id, + user: user.username, + quest, + status, + createdAt: createdAt?.toLocaleString(), + finishedAt: finishedAt?.toLocaleString(), + usedTime: finishedAt ? formatTime(finishedAt - createdAt) : '', + commands, + } +} diff --git a/packages/analytics/src/models/users.js b/packages/analytics/src/models/users.js new file mode 100644 index 0000000..ae1aee6 --- /dev/null +++ b/packages/analytics/src/models/users.js @@ -0,0 +1,46 @@ +import { Session, User } from '@linux-odyssey/models' + +function loginMethods(user) { + const methods = [] + if (user.hashedPassword) methods.push('password') + if (user.google?.id) methods.push('google') + if (user.github?.id) methods.push('github') + return methods +} + +export function userCount() { + return User.find().count() +} + +export async function userList() { + const users = await Session.aggregate([ + { + $group: { + _id: '$user', + count: { $sum: 1 }, + lastActivityAt: { $max: '$lastActivityAt' }, + }, + }, + { + $lookup: { + from: 'users', + localField: '_id', + foreignField: '_id', + as: 'userData', + }, + }, + { $unwind: '$userData' }, + { $sort: { 'userData.createdAt': 1 } }, + ]) + return users.map(({ userData, count, lastActivityAt }) => { + const { username, email, createdAt } = userData + return { + username, + email, + createdAt: createdAt?.toLocaleString(), + method: loginMethods(userData), + sessionCount: count, + lastActivityAt: lastActivityAt?.toLocaleString(), + } + }) +} diff --git a/packages/analytics/src/router.js b/packages/analytics/src/router.js new file mode 100644 index 0000000..f5a458b --- /dev/null +++ b/packages/analytics/src/router.js @@ -0,0 +1,19 @@ +import { Router } from 'express' + +import { homeController } from './controllers/index.js' +import { userListController } from './controllers/users.js' +import { commandListController } from './controllers/commands.js' +import { + sessionDetailController, + sessionListController, +} from './controllers/sessions.js' + +const router = Router() + +router.get('/', homeController) +router.get('/users', userListController) +router.get('/commands', commandListController) +router.get('/sessions', sessionListController) +router.get('/sessions/:id', sessionDetailController) + +export default router diff --git a/packages/analytics/views/commands.handlebars b/packages/analytics/views/commands.handlebars new file mode 100644 index 0000000..16f23c2 --- /dev/null +++ b/packages/analytics/views/commands.handlebars @@ -0,0 +1,24 @@ +

Commands

+
+

Error commands

+ + + + + + + + + + {{#each errorCommands}} + + + + + + + + + {{/each}} +
CommandUserQuestSessionErrorCreated At
{{command}}{{user}}{{quest}}Detail{{error}}{{createdAt}}
+
\ No newline at end of file diff --git a/packages/analytics/views/home.handlebars b/packages/analytics/views/home.handlebars new file mode 100644 index 0000000..cd8148a --- /dev/null +++ b/packages/analytics/views/home.handlebars @@ -0,0 +1,22 @@ +

Welcome to {{title}}

+
+

Users

+ + + + + + + + + + + + + + + + + +
Registered Users{{userCount}}
Session Count{{sessionCount}}
Completed Session{{sessionCompleted}}
Completed Rate{{completedRate}}%
+
\ No newline at end of file diff --git a/packages/analytics/views/layouts/main.handlebars b/packages/analytics/views/layouts/main.handlebars new file mode 100644 index 0000000..e269d7b --- /dev/null +++ b/packages/analytics/views/layouts/main.handlebars @@ -0,0 +1,17 @@ + + + {{title}} + + + + + {{{body}}} + + \ No newline at end of file diff --git a/packages/analytics/views/sessionDetail.handlebars b/packages/analytics/views/sessionDetail.handlebars new file mode 100644 index 0000000..71d8aa6 --- /dev/null +++ b/packages/analytics/views/sessionDetail.handlebars @@ -0,0 +1,56 @@ +

{{title}}

+
+ {{#with session}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Quest{{quest}}
User{{user}}
Status{{status}}
Created At{{createdAt}}
Finished At{{finishedAt}}
Used Time{{usedTime}}
Command Count{{commands.length}}
+ +

Commands

+ + + + + + + + + + {{#each commands}} + + + + + + + + {{/each}} +
CommandPWDOutputErrorCreated At
{{command}}{{pwd}}{{output}}{{error}}{{createdAt}}
+ {{/with}} +
\ No newline at end of file diff --git a/packages/analytics/views/sessions.handlebars b/packages/analytics/views/sessions.handlebars new file mode 100644 index 0000000..6fff971 --- /dev/null +++ b/packages/analytics/views/sessions.handlebars @@ -0,0 +1,27 @@ +

Sessions

+
+ + + + + + + + + + + + {{#each sessions}} + + + + + + + + + + + {{/each}} +
UserQuestStatusCreated AtFinished AtUsed TimeCommand Count
{{user}}{{quest}}Detail{{status}}{{createdAt}}{{finishedAt}}{{usedTime}}{{commandCount}}
+
\ No newline at end of file diff --git a/packages/analytics/views/users.handlebars b/packages/analytics/views/users.handlebars new file mode 100644 index 0000000..acc6233 --- /dev/null +++ b/packages/analytics/views/users.handlebars @@ -0,0 +1,23 @@ +

Users List

+
+ + + + + + + + + + {{#each users}} + + + + + + + + + {{/each}} +
UsernameEmailRegistration MethodRegistration TimeSessions CountLast Activity
{{username}}{{email}}{{method}}{{createdAt}}{{sessionCount}}{{lastActivityAt}}
+
\ No newline at end of file diff --git a/packages/models/package.json b/packages/models/package.json index 1dd49ae..d5f58b0 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -6,7 +6,6 @@ "license": "GPL", "exports": "./index.js", "dependencies": { - "@linux-odyssey/file-graph": "*", "mongoose": "^7.6.0" } } diff --git a/packages/models/session.js b/packages/models/session.js index ddb4fad..85568a1 100644 --- a/packages/models/session.js +++ b/packages/models/session.js @@ -1,5 +1,4 @@ import { model, Schema } from 'mongoose' -import { FileNode } from '@linux-odyssey/file-graph' const nodeSchema = new Schema({ path: String, @@ -58,10 +57,10 @@ const Session = model( hints: [String], graph: { type: nodeSchema, - default: new FileNode({ + default: { path: '/', type: 'folder', - }), + }, }, }, { timestamps: true } diff --git a/server/src/auth/oauthVerify.js b/server/src/auth/oauthVerify.js index ce20d19..409fde3 100644 --- a/server/src/auth/oauthVerify.js +++ b/server/src/auth/oauthVerify.js @@ -15,7 +15,10 @@ export default async function oauthVerify(provider, profile, query, cb) { newUser[provider] = profile return cb(null, { newUser }) } - user[provider] = profile + user[provider] = { + id: profile.id, + displayName: profile.displayName, + } await user.save() } diff --git a/server/src/containers/expiryChecker.js b/server/src/containers/expiryChecker.js index 41699cc..3ecc30c 100644 --- a/server/src/containers/expiryChecker.js +++ b/server/src/containers/expiryChecker.js @@ -16,7 +16,9 @@ export async function removeExpired() { sessions.map(async (session) => { console.log('Expiring session: ', session._id, session.containerId) await deleteContainer(session.containerId) - session.status = 'inactive' + if (session.status === 'active') { + session.status = 'inactive' + } session.containerId = null await session.save() }) diff --git a/yarn.lock b/yarn.lock index 33272f0..0046d79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -547,6 +547,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + 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" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -835,6 +847,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@pkgr/utils@^2.3.1": version "2.4.2" resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.4.2.tgz#9e638bbe9a6a6f165580dc943f138fd3309a2cbc" @@ -1182,6 +1199,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -1201,6 +1223,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -1547,6 +1574,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -2148,6 +2182,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -2183,6 +2222,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -2633,6 +2677,15 @@ expect@^29.6.1: jest-message-util "^29.6.1" jest-util "^29.6.1" +express-handlebars@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/express-handlebars/-/express-handlebars-7.1.2.tgz#2471673d11af46f496cba4098a705f0217232fda" + integrity sha512-ss9d3mBChOLTEtyfzXCsxlItUxpgS3i4cb/F70G6Q5ohQzmD12XB4x/Y9U6YboeeYBJZt7WQ5yUNu7ZSQ/EGyQ== + dependencies: + glob "^10.3.3" + graceful-fs "^4.2.11" + handlebars "^4.7.8" + express-session@^1.17.3: version "1.17.3" resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.3.tgz#14b997a15ed43e5949cb1d073725675dd2777f36" @@ -2837,6 +2890,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -3027,6 +3088,17 @@ glob@7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^10.3.3: + version "10.3.10" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" + integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -3079,7 +3151,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3089,6 +3161,18 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +handlebars@^4.7.8: + version "4.7.8" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.2" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -3530,6 +3614,15 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +jackspeak@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jest-changed-files@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e" @@ -4178,6 +4271,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +"lru-cache@^9.1.1 || ^10.0.0": + version "10.0.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" + integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== + luxon@^3.2.1: version "3.4.1" resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.1.tgz#9147374b2c539e7893f906c420e9b080b59c5457" @@ -4281,7 +4379,14 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: +minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -4298,6 +4403,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -4428,6 +4538,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + node-addon-api@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" @@ -4792,6 +4907,14 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -5272,6 +5395,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + simple-update-notifier@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" @@ -5444,7 +5572,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5453,6 +5581,15 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string.prototype.trim@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" @@ -5487,13 +5624,20 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -5827,6 +5971,11 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" +uglify-js@^3.1.4: + version "3.17.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" + integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== + uid-safe@~2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" @@ -6047,6 +6196,20 @@ wide-align@^1.1.2: dependencies: string-width "^1.0.2 || 2 || 3 || 4" +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -6056,14 +6219,14 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" wrappy@1: version "1.0.2"