diff --git a/.git-hooks/commit-msg b/.git-hooks/commit-msg new file mode 100644 index 00000000..d29127d2 --- /dev/null +++ b/.git-hooks/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +set -e + +npx --yes @commitlint/cli@20.4.3 --edit "$1" \ No newline at end of file diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index d98f594b..aaad8add 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -1,29 +1,29 @@ -name: Commit Compliance +name: Commit Message Check on: pull_request: types: [opened, synchronize, reopened] jobs: - commitlint: + commit-check: runs-on: ubuntu-latest steps: - - name: Checkout code with submodule - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: - ref: ${{ github.ref }} - submodules: true fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: '20' - - name: Install dependencies - run: npm ci --legacy-peer-deps - - - name: Run commitlint on PR + - name: Validate commit messages run: | - npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose \ No newline at end of file + set -eo pipefail + commits=$(git log --no-merges --pretty=format:"%H" "${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}") + for sha in $commits; do + tmp=$(mktemp) + git log -1 --pretty=format:"%B" "$sha" > "$tmp" + npx --yes @commitlint/cli@20.4.3 --edit "$tmp" + rm "$tmp" + done diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100644 index b6d72ffd..00000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -npx --no -- commitlint --edit "$1" \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 754fb2e4..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -npx lint-staged \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..913e1435 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,98 @@ +# CLAUDE.md - Common-API + +## Project Overview + +Common-API is the gateway microservice for the AMRIT healthcare platform. It provides shared APIs consumed by all frontend UIs including authentication, beneficiary registration, call handling, location masters, notifications, feedback, reporting, and integrations with external systems (c-Zentrix CTI, Everwell, eAusadha, eSanjeevani, ABDM, Firebase, Honeywell POCT devices). + +## Tech Stack + +- Java 17 +- Spring Boot 3.2.2 +- Spring Data JPA / Hibernate +- MySQL 8.0 +- Redis (session management, caching) +- MongoDB (optional, for specific integrations) +- Maven (build tool) +- Swagger/OpenAPI (API documentation) +- Lombok, MapStruct +- CryptoJS-compatible AES encryption +- Firebase Admin SDK +- WAR packaging (deploys to Wildfly) + +## Build and Run + +```bash +# Build +mvn clean install -DENV_VAR=local + +# Run locally (start Redis first) +mvn spring-boot:run -DENV_VAR=local + +# Package WAR +mvn -B package --file pom.xml -P # profiles: dev, local, test, ci, uat + +# Run tests +mvn test +``` + +### Configuration + +- Copy `src/main/environment/common_example.properties` to `common_local.properties` and edit. +- Environment selected via `-DENV_VAR=`. +- Swagger UI: `http://localhost:8083/swagger-ui.html` + +## Package Structure + +Base package: `com.iemr.common` + +| Layer | Package | Description | +|-------|---------|-------------| +| Controllers | `controller.*` | REST endpoints (40+ sub-packages) | +| Services | `service.*` | Business logic | +| Repositories | `repository.*`, `repo.*` | JPA repositories | +| Entities | `data.*` | JPA entity classes | +| DTOs | `model.*` | Transfer objects | +| Mappers | `mapper.*` | Object mapping | +| Config | `config.*` | Swagger, encryption, Firebase, Quartz, prototypes | +| Constants | `constant` | Application constants | +| Utils | `utils.*` | Redis, HTTP, session, validation, exception | + +## Key Functional Domains + +- **Authentication/Authorization**: `controller.users` - login, session, user management +- **Beneficiary Registration**: `controller.beneficiary` - create, search, update beneficiaries +- **Call Handling**: `controller.callhandling` - CTI integration, call lifecycle +- **Feedback/Grievance**: `controller.feedback`, `controller.grievance` - feedback and complaint management +- **Location**: `controller.location` - state, district, block, village masters +- **Notifications**: `controller.notification` - alerts, SMS, email, Firebase push +- **Reporting**: `controller.report`, `controller.secondaryReport` - CRM reports +- **Helpline 104**: `controller.helpline104history` - medical advice history +- **COVID**: `controller.covid` - vaccination status +- **CTI Integration**: `controller.cti` - c-Zentrix computer telephony +- **External Integrations**: `controller.eausadha`, `controller.esanjeevani`, `controller.everwell`, `controller.honeywell`, `controller.brd`, `controller.carestream` +- **ABDM**: `controller.abdmfacility` - Ayushman Bharat Digital Mission +- **KM File Management**: `controller.kmfilemanager` - OpenKM document management +- **OTP/SMS**: `controller.otp`, `controller.sms` (via SMS gateway) +- **Scheduling**: `controller.questionconfig`, `controller.scheme` +- **Door-to-Door App**: `controller.door_to_door_app` - field worker support +- **NHM Dashboard**: `controller.nhmdashboard` - National Health Mission integration + +## Architecture Notes + +- Entry point: `CommonMain.java` (main class in `utils` package) +- Acts as the API gateway; all frontend UIs authenticate through Common-API +- Session management via Redis with 27-minute timeout +- HTTP interceptors attach `Authorization` and `ServerAuthorization` headers +- Status code `5002` signals session expiration to frontends +- AES + PBKDF2 encryption for password handling (`config.encryption`) +- Firebase integration for push notifications (`config.firebase`) +- Quartz scheduler for background jobs (`config.quartz`) +- Extensive test coverage with unit tests under `src/test/` + +## CI/CD + +- GitHub Actions: `package.yml`, `build-on-pull-request.yml`, `sast.yml`, `commit-lint.yml`, `codeql.yml` +- Conventional Commits enforced via Husky + commitlint +- Checkstyle configuration in `checkstyle.xml` +- JaCoCo for code coverage, SonarQube integration configured +- Dockerfile for containerized deployment diff --git a/README.md b/README.md index 169fc005..05d19831 100644 --- a/README.md +++ b/README.md @@ -35,47 +35,8 @@ Prerequisites Detailed information on API endpoints can be found in the [API Guide](https://piramal-swasthya.gitbook.io/amrit/architecture/api-guide). ## Setting Up Commit Hooks - -This project uses Git hooks to enforce consistent code quality and commit message standards. Even though this is a Java project, the hooks are powered by Node.js. Follow these steps to set up the hooks locally: - -### Prerequisites -- Node.js (v18 or later) -- npm (comes with Node.js) - -### Setup Steps - -1. **Install Node.js and npm** - - Download and install from [nodejs.org](https://nodejs.org/) - - Verify installation with: - ``` - node --version - npm --version - ``` -2. **Install dependencies** - - From the project root directory, run: - ``` - npm ci - ``` - - This will install all required dependencies including Husky and commitlint -3. **Verify hooks installation** - - The hooks should be automatically installed by Husky - - You can verify by checking if the `.husky` directory contains executable hooks -### Commit Message Convention -This project follows a specific commit message format: -- Format: `type(scope): subject` -- Example: `feat(login): add remember me functionality` -Types include: -- `feat`: A new feature -- `fix`: A bug fix -- `docs`: Documentation changes -- `style`: Code style changes (formatting, etc.) -- `refactor`: Code changes that neither fix bugs nor add features -- `perf`: Performance improvements -- `test`: Adding or fixing tests -- `build`: Changes to build process or tools -- `ci`: Changes to CI configuration -- `chore`: Other changes (e.g., maintenance tasks, dependencies) -Your commit messages will be automatically validated when you commit, ensuring project consistency. +Enable git hooks (run once after cloning): + - Run the command `git config core.hooksPath .git-hooks`. ## Usage All features have been exposed as REST endpoints. Refer to the SWAGGER API specification for details. diff --git a/commitlint.config.js b/commitlint.config.js index e65d7e68..abc565a9 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,5 +1,4 @@ module.exports = { - extends: ['@commitlint/config-conventional'], rules: { 'body-leading-blank': [1, 'always'], 'body-max-line-length': [2, 'always', 100], @@ -32,5 +31,5 @@ module.exports = { 'test', ], ], - } + }, }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index a4145e97..00000000 --- a/package-lock.json +++ /dev/null @@ -1,3433 +0,0 @@ -{ - "name": "mmu-api", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mmu-api", - "version": "1.0.0", - "devDependencies": { - "@commitlint/cli": "^19.8.0", - "@commitlint/config-conventional": "^19.8.0", - "commitizen": "^4.3.1", - "cz-conventional-changelog": "^3.3.0", - "husky": "^9.1.7", - "lint-staged": "^15.5.1" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@commitlint/cli": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.1.tgz", - "integrity": "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/format": "^19.8.1", - "@commitlint/lint": "^19.8.1", - "@commitlint/load": "^19.8.1", - "@commitlint/read": "^19.8.1", - "@commitlint/types": "^19.8.1", - "tinyexec": "^1.0.0", - "yargs": "^17.0.0" - }, - "bin": { - "commitlint": "cli.js" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/config-conventional": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-19.8.1.tgz", - "integrity": "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.8.1", - "conventional-changelog-conventionalcommits": "^7.0.2" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/config-validator": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.8.1.tgz", - "integrity": "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.8.1", - "ajv": "^8.11.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/ensure": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-19.8.1.tgz", - "integrity": "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.8.1", - "lodash.camelcase": "^4.3.0", - "lodash.kebabcase": "^4.1.1", - "lodash.snakecase": "^4.1.1", - "lodash.startcase": "^4.4.0", - "lodash.upperfirst": "^4.3.1" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/execute-rule": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-19.8.1.tgz", - "integrity": "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/format": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-19.8.1.tgz", - "integrity": "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.8.1", - "chalk": "^5.3.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/is-ignored": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-19.8.1.tgz", - "integrity": "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.8.1", - "semver": "^7.6.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/lint": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-19.8.1.tgz", - "integrity": "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/is-ignored": "^19.8.1", - "@commitlint/parse": "^19.8.1", - "@commitlint/rules": "^19.8.1", - "@commitlint/types": "^19.8.1" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/load": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-19.8.1.tgz", - "integrity": "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/config-validator": "^19.8.1", - "@commitlint/execute-rule": "^19.8.1", - "@commitlint/resolve-extends": "^19.8.1", - "@commitlint/types": "^19.8.1", - "chalk": "^5.3.0", - "cosmiconfig": "^9.0.0", - "cosmiconfig-typescript-loader": "^6.1.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "lodash.uniq": "^4.5.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/message": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-19.8.1.tgz", - "integrity": "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/parse": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-19.8.1.tgz", - "integrity": "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^19.8.1", - "conventional-changelog-angular": "^7.0.0", - "conventional-commits-parser": "^5.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/read": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-19.8.1.tgz", - "integrity": "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/top-level": "^19.8.1", - "@commitlint/types": "^19.8.1", - "git-raw-commits": "^4.0.0", - "minimist": "^1.2.8", - "tinyexec": "^1.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/resolve-extends": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-19.8.1.tgz", - "integrity": "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/config-validator": "^19.8.1", - "@commitlint/types": "^19.8.1", - "global-directory": "^4.0.1", - "import-meta-resolve": "^4.0.0", - "lodash.mergewith": "^4.6.2", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/rules": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-19.8.1.tgz", - "integrity": "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/ensure": "^19.8.1", - "@commitlint/message": "^19.8.1", - "@commitlint/to-lines": "^19.8.1", - "@commitlint/types": "^19.8.1" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/to-lines": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-19.8.1.tgz", - "integrity": "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/top-level": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-19.8.1.tgz", - "integrity": "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^7.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/types": { - "version": "19.8.1", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.8.1.tgz", - "integrity": "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/conventional-commits-parser": "^5.0.0", - "chalk": "^5.3.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@types/conventional-commits-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", - "integrity": "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.12.0" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "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-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/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "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/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "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/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "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/cachedir": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", - "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true, - "license": "MIT" - }, - "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-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "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/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 10" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "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/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/commitizen": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.1.tgz", - "integrity": "sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cachedir": "2.3.0", - "cz-conventional-changelog": "3.3.0", - "dedent": "0.7.0", - "detect-indent": "6.1.0", - "find-node-modules": "^2.1.2", - "find-root": "1.1.0", - "fs-extra": "9.1.0", - "glob": "7.2.3", - "inquirer": "8.2.5", - "is-utf8": "^0.2.1", - "lodash": "4.17.21", - "minimist": "1.2.7", - "strip-bom": "4.0.0", - "strip-json-comments": "3.1.1" - }, - "bin": { - "commitizen": "bin/commitizen", - "cz": "bin/git-cz", - "git-cz": "bin/git-cz" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/commitizen/node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/conventional-changelog-angular": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", - "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/conventional-changelog-conventionalcommits": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", - "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/conventional-commit-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-3.0.0.tgz", - "integrity": "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==", - "dev": true, - "license": "ISC" - }, - "node_modules/conventional-commits-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", - "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-text-path": "^2.0.0", - "JSONStream": "^1.3.5", - "meow": "^12.0.1", - "split2": "^4.0.0" - }, - "bin": { - "conventional-commits-parser": "cli.mjs" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig-typescript-loader": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.1.0.tgz", - "integrity": "sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "jiti": "^2.4.1" - }, - "engines": { - "node": ">=v18" - }, - "peerDependencies": { - "@types/node": "*", - "cosmiconfig": ">=9", - "typescript": ">=5" - } - }, - "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/cz-conventional-changelog": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", - "integrity": "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^2.4.1", - "commitizen": "^4.0.3", - "conventional-commit-types": "^3.0.0", - "lodash.map": "^4.5.1", - "longest": "^2.0.1", - "word-wrap": "^1.0.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@commitlint/load": ">6.1.1" - } - }, - "node_modules/cz-conventional-changelog/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cz-conventional-changelog/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/dargs": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", - "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true, - "license": "MIT" - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "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/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, - "license": "MIT" - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "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/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-node-modules": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.3.tgz", - "integrity": "sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "findup-sync": "^4.0.0", - "merge": "^2.1.1" - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true, - "license": "MIT" - }, - "node_modules/find-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", - "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^7.2.0", - "path-exists": "^5.0.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/findup-sync": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", - "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^4.0.2", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "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/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "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==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/git-raw-commits": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", - "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dargs": "^8.0.0", - "meow": "^12.0.1", - "split2": "^4.0.0" - }, - "bin": { - "git-raw-commits": "cli.mjs" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "4.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "license": "MIT", - "dependencies": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "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": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.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/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-meta-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", - "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/inquirer/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/inquirer/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/inquirer/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-text-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", - "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "text-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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/jiti": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", - "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ], - "license": "MIT" - }, - "node_modules/JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "license": "(MIT OR Apache-2.0)", - "dependencies": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - }, - "bin": { - "JSONStream": "bin.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lint-staged": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", - "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.4.1", - "commander": "^13.1.0", - "debug": "^4.4.0", - "execa": "^8.0.1", - "lilconfig": "^3.1.3", - "listr2": "^8.2.5", - "micromatch": "^4.0.8", - "pidtree": "^0.6.0", - "string-argv": "^0.3.2", - "yaml": "^2.7.0" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/listr2": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", - "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^4.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "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/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.startcase": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", - "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.upperfirst": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", - "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", - "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-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-symbols/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", - "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "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/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/longest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz", - "integrity": "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/meow": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", - "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", - "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", - "dev": true, - "license": "MIT" - }, - "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/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "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/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true, - "license": "ISC" - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ora/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==", - "dev": true, - "license": "MIT" - }, - "node_modules/ora/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "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==", - "dev": true, - "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", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/restore-cursor/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/restore-cursor/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": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/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/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/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "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/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/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "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/string-width/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/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-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/text-extensions": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", - "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "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/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "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/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "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/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/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/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/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==", - "dev": true, - "license": "MIT" - }, - "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", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 79df8180..00000000 --- a/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "common-api", - "version": "1.0.0", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "prepare": "husky", - "commit": "cz" - }, - "config": { - "commitizen": { - "path": "./node_modules/cz-conventional-changelog" - } - }, - "lint-staged": { - "src/**/*.{ts,js}": [ - "eslint --fix" - ], - "**/*.java": [ - "java -jar checkstyle.jar -c checkstyle.xml " - ], - "**/*.xml": [ - "xmllint --noout " - ] - }, - "devDependencies": { - "@commitlint/cli": "^19.8.0", - "@commitlint/config-conventional": "^19.8.0", - "commitizen": "^4.3.1", - "cz-conventional-changelog": "^3.3.0", - "husky": "^9.1.7", - "lint-staged": "^15.5.1" - } -} diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index f2b774a3..547e069f 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -3,12 +3,12 @@ spring.datasource.url=@env.DATABASE_URL@ spring.datasource.username=@env.DATABASE_USERNAME@ spring.datasource.password=@env.DATABASE_PASSWORD@ -spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver secondary.datasource.username=@env.REPORTING_DATABASE_USERNAME@ secondary.datasource.password=@env.REPORTING_DATABASE_PASSWORD@ secondary.datasource.url=@env.REPORTING_DATABASE_URL@ -secondary.datasource.driver-class-name=com.mysql.jdbc.Driver +secondary.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ## KM Configuration km-base-protocol=@env.KM_API_BASE_PROTOCOL@ @@ -190,10 +190,19 @@ captcha.enable-captcha=@env.ENABLE_CAPTCHA@ cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ -video-call-url=@env.VIDEO_CALL_URL@ -jibri.output.path=@env.JIBRI_OUTPUT_PATH@ +# Jitsi configuration +videocall.url=@env.VIDEO_CALL_URL@ video.recording.path=@env.VIDEO_RECORDING_PATH@ +# Jitsi JWT (prosody token-auth) +jitsi.app.id=@env.JITSI_APP_ID@ +jitsi.app.secret=@env.JITSI_APP_SECRET@ +jitsi.domain=@env.JITSI_DOMAIN@ +jitsi.sub=@env.JITSI_SUB@ +jitsi.token.ttl.seconds=@env.JITSI_TOKEN_TTL_SECONDS@ +jitsi.room.prefix=@env.JITSI_ROOM_PREFIX@ +jitsi.default.user.email=@env.JITSI_DEFAULT_USER_EMAIL@ + platform.feedback.ratelimit.enabled=@env.PLATFORM_FEEDBACK_RATELIMIT_ENABLED@ platform.feedback.ratelimit.pepper=@env.PLATFORM_FEEDBACK_RATELIMIT_PEPPER@ platform.feedback.ratelimit.trust-forwarded-for=@env.PLATFORM_FEEDBACK_RATELIMIT_TRUST_FORWARDED_FOR@ diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index a5c633e4..064090b3 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -3,12 +3,12 @@ spring.datasource.url=${DATABASE_URL} spring.datasource.username=${DATABASE_USERNAME} spring.datasource.password=${DATABASE_PASSWORD} -spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver secondary.datasource.username=${REPORTING_DATABASE_USERNAME} secondary.datasource.password=${REPORTING_DATABASE_PASSWORD} secondary.datasource.url=${REPORTING_DATABASE_URL} -secondary.datasource.driver-class-name=com.mysql.jdbc.Driver +secondary.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ## KM Configuration km-base-protocol=${KM_API_BASE_PROTOCOL} @@ -192,10 +192,18 @@ firebase.enabled=${FIREBASE_ENABLE} firebase.credential-file=${FIREBASE_CREDENTIAL} -video-call-url=${VIDEO_CALL_URL} -jibri.output.path={JIBRI_OUTPUT_PATH} +videocall.url=${VIDEO_CALL_URL} video.recording.path={VIDEO_RECORDING_PATH} +# Jitsi JWT (prosody token-auth) +jitsi.app.id=${JITSI_APP_ID} +jitsi.app.secret=${JITSI_APP_SECRET} +jitsi.domain=${JITSI_DOMAIN} +jitsi.sub=${JITSI_SUB} +jitsi.token.ttl.seconds=${JITSI_TOKEN_TTL_SECONDS} +jitsi.room.prefix=${JITSI_ROOM_PREFIX} +jitsi.default.user.email=${JITSI_DEFAULT_USER_EMAIL} + # Platform Feedback module platform.feedback.ratelimit.enabled=${PLATFORM_FEEDBACK_RATELIMIT_ENABLED} platform.feedback.ratelimit.pepper=${PLATFORM_FEEDBACK_RATELIMIT_PEPPER} diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index e3b5c031..ac440991 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -7,12 +7,12 @@ spring.datasource.password=1234 encDbUserName=zFlYsp9Z0s+lRvLM15A3g/Ba0w8VGs/1usuW7EsGF3k= encDbPass=JGGAGn5wTlrbTLUHY+5BzfBa0w8VGs/1usuW7EsGF3k= -spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver secondary.datasource.username=root secondary.datasource.password=1234 secondary.datasource.url=jdbc:mysql://localhost:3306/db_reporting -secondary.datasource.driver-class-name=com.mysql.jdbc.Driver +secondary.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ## KM Configuration @@ -25,7 +25,7 @@ km-root-path=/okm:personal/users/ km-guest-user=guest km-guest-password=guest -tempFilePath=/opt/openkm +tempFilePath=/tmp # CTI Config cti-server-ip=10.208.122.99 @@ -42,6 +42,7 @@ genben-api=http://localhost:8092 send-sms=false sendSMSUrl = http://localhost:8083/sms/sendSMS source-address=AIDSHL +sms-consent-source-address= sms-username= sms-password= send-message-url= @@ -200,9 +201,9 @@ grievanceAllocationRetryConfiguration=3 logging.path=logs/ logging.file.name=logs/common-api.log -video-call-url=https://vc.piramalswasthya.org/? -jibri.output.path=/srv/jibri/recordings -video.recording.path=/srv/recordings +# Jitsi configuration +videocall.url=https://vc.piramalswasthya.org/? +video.recording.path=/opt/recordings captcha.secret-key= captcha.verify-url= https://challenges.cloudflare.com/turnstile/v0/siteverify @@ -233,3 +234,11 @@ otp.ratelimit.day-limit=20 ### generate Beneficiary IDs URL generateBeneficiaryIDs-api-url=/generateBeneficiaryController/generateBeneficiaryIDs + +jitsi.app.id= +jitsi.app.secret= +jitsi.domain= +jitsi.sub=meet.jitsi +jitsi.token.ttl.seconds=3600 +jitsi.room.prefix=piramal-meeting- +jitsi.default.user.email= \ No newline at end of file diff --git a/src/main/java/com/iemr/common/CommonApplication.java b/src/main/java/com/iemr/common/CommonApplication.java index e4a59994..45d61800 100644 --- a/src/main/java/com/iemr/common/CommonApplication.java +++ b/src/main/java/com/iemr/common/CommonApplication.java @@ -29,6 +29,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.client.RestTemplate; @@ -40,6 +41,7 @@ @SpringBootApplication @EnableScheduling +@EnableAsync(proxyTargetClass = true) public class CommonApplication extends SpringBootServletInitializer { @Bean diff --git a/src/main/java/com/iemr/common/config/InterceptorConfig.java b/src/main/java/com/iemr/common/config/InterceptorConfig.java index a321eb08..8a45482a 100644 --- a/src/main/java/com/iemr/common/config/InterceptorConfig.java +++ b/src/main/java/com/iemr/common/config/InterceptorConfig.java @@ -36,7 +36,8 @@ public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(requestInterceptor); + registry.addInterceptor(requestInterceptor) + .excludePathPatterns("/video-consultation/resolve", "**/video-consultation/resolve"); } } \ No newline at end of file diff --git a/src/main/java/com/iemr/common/config/encryption/SecurePassword.java b/src/main/java/com/iemr/common/config/encryption/SecurePassword.java index 15463b7a..95cdd7f3 100644 --- a/src/main/java/com/iemr/common/config/encryption/SecurePassword.java +++ b/src/main/java/com/iemr/common/config/encryption/SecurePassword.java @@ -26,10 +26,8 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; - import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; - import org.springframework.stereotype.Service; @Service diff --git a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java index 0fb758b0..d2b19d77 100644 --- a/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java +++ b/src/main/java/com/iemr/common/controller/beneficiary/BeneficiaryRegistrationController.java @@ -37,6 +37,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -74,6 +75,8 @@ import com.iemr.common.service.userbeneficiarydata.TitleService; import com.iemr.common.utils.CookieUtil; import com.iemr.common.utils.JwtUtil; +import com.iemr.common.utils.CookieUtil; +import com.iemr.common.utils.JwtUtil; import com.iemr.common.utils.mapper.InputMapper; import com.iemr.common.utils.mapper.OutputMapper; import com.iemr.common.utils.response.OutputResponse; @@ -665,9 +668,16 @@ public String getBeneficiaryIDs( } private void setBeneficiaryGender(List iBeneficiary) { + if (iBeneficiary == null || iBeneficiary.isEmpty()) { + return; + } for (BeneficiaryModel beneficiaryModel : iBeneficiary) { - if (null != beneficiaryModel.getM_gender() && beneficiaryModel.getM_gender().getGenderName() != null) + if (beneficiaryModel == null) { + continue; + } + if (beneficiaryModel.getM_gender() != null && beneficiaryModel.getM_gender().getGenderName() != null) { beneficiaryModel.setGenderName(beneficiaryModel.getM_gender().getGenderName()); + } } } diff --git a/src/main/java/com/iemr/common/controller/connect/ConnectController.java b/src/main/java/com/iemr/common/controller/connect/ConnectController.java new file mode 100644 index 00000000..7ecf45ba --- /dev/null +++ b/src/main/java/com/iemr/common/controller/connect/ConnectController.java @@ -0,0 +1,57 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.controller.connect; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.iemr.common.utils.NetworkUtil; + +import io.swagger.v3.oas.annotations.Operation; + +/** + * Exposes the server's LAN address so a mobile device on the same wifi + * network can connect to this API. + */ +@RestController +@RequestMapping(value = "/public/connect") +public class ConnectController { + + @Value("${server.port:8080}") + private int serverPort; + + @Operation(summary = "Get the server's LAN IP address and port") + @GetMapping(value = "/info", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getConnectInfo() { + Map response = new LinkedHashMap<>(); + response.put("ip", NetworkUtil.getLanIPAddress()); + response.put("port", serverPort); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java index 62bf7e7c..c24651c6 100644 --- a/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java +++ b/src/main/java/com/iemr/common/controller/dynamicForm/DynamicFormController.java @@ -97,4 +97,6 @@ public ResponseEntity> getStructuredForm(@PathVariable String for } + + } diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index f3612fdb..d127fe3c 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -79,8 +79,10 @@ @RequestMapping("/user") @RestController public class IEMRAdminController { + private static final String USER_ID_FIELD = "userId"; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private InputMapper inputMapper = new InputMapper(); + private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("provideradmin", "superadmin"); // @Value("${captcha.enable-captcha}") private boolean enableCaptcha =false; @@ -190,11 +192,22 @@ public String userAuthenticate( if (m_User.getUserName() != null && (m_User.getDoLogout() == null || !m_User.getDoLogout()) && (m_User.getWithCredentials() != null && m_User.getWithCredentials())) { - String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( - m_User.getUserName().trim().toLowerCase()); - if (tokenFromRedis != null) { - throw new IEMRException( - "You are already logged in,please confirm to logout from other device and login again"); + String userRole = ""; + if (mUser.size() == 1 && mUser.get(0).getM_UserServiceRoleMapping() != null) { + for (UserServiceRoleMapping usrm : mUser.get(0).getM_UserServiceRoleMapping()) { + if (usrm.getM_Role() != null && usrm.getM_Role().getRoleName() != null) { + userRole = usrm.getM_Role().getRoleName(); + break; + } + } + } + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { + String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( + m_User.getUserName().trim().toLowerCase()); + if (tokenFromRedis != null) { + throw new IEMRException( + "You are already logged in,please confirm to logout from other device and login again"); + } } } else if (m_User.getUserName() != null && m_User.getDoLogout() != null && m_User.getDoLogout() == true) { deleteSessionObject(m_User.getUserName().trim().toLowerCase()); @@ -346,6 +359,16 @@ public ResponseEntity refreshToken(@RequestBody Map request) String userId = claims.get("userId", String.class); User user = iemrAdminUserServiceImpl.getUserById(Long.parseLong(userId)); + // validate if user account is locked or de-activated + if(user.getDeleted()){ + logger.warn("Your account is locked or de-activated. Please contact administrator"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Your account is locked or de-activated. Please contact administrator."); + } + if(user.getStatusID()>2){ + logger.warn("Your account is not active. Please contact administrator"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Your account is not active. Please contact administrator."); + } + // Validate that the user still exists and is active if (user == null) { logger.warn("Token validation failed: user not found for userId in token."); @@ -411,16 +434,28 @@ public String logOutUserFromConcurrentSession( deleteSessionObjectByGettingSessionDetails(previousTokenFromRedis); sessionObject.deleteSessionObject(previousTokenFromRedis); - // Denylist the active JWT so System 1's requests are immediately rejected - String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); - String jtiData = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); - if (jtiData != null) { - String[] parts = jtiData.split("\\|", 2); - tokenDenylist.addTokenToDenylist(parts[0], jwtUtil.getAccessTokenExpiration()); - if (parts.length > 1) { - redisTemplate.delete("user_" + parts[1]); + String userRole = ""; + if (mUsers.get(0).getM_UserServiceRoleMapping() != null) { + for (UserServiceRoleMapping usrm : mUsers.get(0).getM_UserServiceRoleMapping()) { + if (usrm.getM_Role() != null && usrm.getM_Role().getRoleName() != null) { + userRole = usrm.getM_Role().getRoleName(); + break; + } + } + } + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { + // Denylist the active JWT so the first system's requests are immediately rejected + String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); + String jtiData = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); + if (jtiData != null) { + String[] parts = jtiData.split("\\|", 2); + String jti = parts[0]; + tokenDenylist.addTokenToDenylist(jti, jwtUtil.getAccessTokenExpiration()); + if (parts.length > 1) { + redisTemplate.delete("user_" + parts[1]); + } + stringRedisTemplate.delete("jti:" + usernameKey); } - stringRedisTemplate.delete("jti:" + usernameKey); } response.setResponse("User successfully logged out"); @@ -535,11 +570,13 @@ public String superUserAuthenticate( String refreshToken = null; boolean isMobile = false; if (m_User.getUserName() != null && (m_User.getDoLogout() == null || m_User.getDoLogout() == false)) { - String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( - m_User.getUserName().trim().toLowerCase()); - if (tokenFromRedis != null) { - throw new IEMRException( - "You are already logged in,please confirm to logout from other device and login again"); + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(m_User.getUserName().trim().toLowerCase())) { + String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( + m_User.getUserName().trim().toLowerCase()); + if (tokenFromRedis != null) { + throw new IEMRException( + "You are already logged in,please confirm to logout from other device and login again"); + } } } else if (m_User.getUserName() != null && m_User.getDoLogout() != null && m_User.getDoLogout() == true) { deleteSessionObject(m_User.getUserName().trim().toLowerCase()); @@ -671,6 +708,13 @@ public String getLoginResponse(HttpServletRequest request) { throw new IEMRException("Authentication failed. Please log in again."); } + // Validate the token first + Claims claims = jwtUtil.validateToken(jwtToken); + if (claims == null) { + logger.warn("Authentication failed: invalid or expired token."); + throw new IEMRException("Authentication failed. Please log in again."); + } + // Extract user ID from the JWT token String userId = jwtUtil.getUserIdFromToken(jwtToken); @@ -991,6 +1035,13 @@ public String userLogout(HttpServletRequest request) { try { deleteSessionObjectByGettingSessionDetails(request.getHeader("Authorization")); sessionObject.deleteSessionObject(request.getHeader("Authorization")); + try { + stringRedisTemplate.delete("camp:vanID"); + stringRedisTemplate.delete("camp:parkingPlaceID"); + logger.info("Camp config cleared from Redis on MMU logout"); + } catch (Exception redisEx) { + logger.warn("Failed to clear camp Redis keys on logout: {}", redisEx.getMessage()); + } response.setResponse("Success"); } catch (Exception e) { response.setError(e); @@ -1336,4 +1387,104 @@ public ResponseEntity checkUserDetails(@PathVariable("userName") String userN } } + + @Operation(summary = "Lock user account") + @PostMapping(value = "/lockUserAccount", produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String lockUserAccount(@RequestBody String request, HttpServletRequest httpRequest) { + OutputResponse response = new OutputResponse(); + try { + Long authenticatedUserId = getAuthenticatedUserId(httpRequest); + validateAdminPrivileges(authenticatedUserId); + Long userId = parseUserIdFromRequest(request); + boolean locked = iemrAdminUserServiceImpl.lockUserAccount(userId); + response.setResponse(locked ? "User account successfully locked" : "User account was already locked"); + } catch (Exception e) { + logger.error("Error locking user account: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Unlock user account locked due to failed login attempts") + @PostMapping(value = "/unlockUserAccount", produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String unlockUserAccount(@RequestBody String request, HttpServletRequest httpRequest) { + OutputResponse response = new OutputResponse(); + try { + Long authenticatedUserId = getAuthenticatedUserId(httpRequest); + validateAdminPrivileges(authenticatedUserId); + Long userId = parseUserIdFromRequest(request); + boolean unlocked = iemrAdminUserServiceImpl.unlockUserAccount(userId); + response.setResponse(unlocked ? "User account successfully unlocked" : "User account was not locked"); + } catch (Exception e) { + logger.error("Error unlocking user account: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get user account lock status") + @PostMapping(value = "/getUserLockStatus", produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String getUserLockStatus(@RequestBody String request, HttpServletRequest httpRequest) { + OutputResponse response = new OutputResponse(); + try { + Long authenticatedUserId = getAuthenticatedUserId(httpRequest); + validateAdminPrivileges(authenticatedUserId); + Long userId = parseUserIdFromRequest(request); + String lockStatusJson = iemrAdminUserServiceImpl.getUserLockStatusJson(userId); + response.setResponse(lockStatusJson); + } catch (Exception e) { + logger.error("Error getting user lock status: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + private Long parseUserIdFromRequest(String request) throws IEMRException { + try { + JsonObject requestObj = JsonParser.parseString(request).getAsJsonObject(); + if (!requestObj.has(USER_ID_FIELD) || requestObj.get(USER_ID_FIELD).isJsonNull()) { + throw new IEMRException(USER_ID_FIELD + " is required"); + } + JsonElement userIdElement = requestObj.get(USER_ID_FIELD); + if (!userIdElement.isJsonPrimitive() || !userIdElement.getAsJsonPrimitive().isNumber()) { + throw new IEMRException(USER_ID_FIELD + " must be a number"); + } + return userIdElement.getAsLong(); + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Failed to parse {} from request: {}", USER_ID_FIELD, e.getMessage()); + throw new IEMRException("Invalid request body"); + } + } + + private Long getAuthenticatedUserId(HttpServletRequest httpRequest) throws IEMRException { + String authorization = httpRequest.getHeader("Authorization"); + if (authorization != null && authorization.contains("Bearer ")) { + authorization = authorization.replace("Bearer ", ""); + } + if (authorization == null || authorization.isEmpty()) { + throw new IEMRException("Authentication required"); + } + try { + String sessionJson = sessionObject.getSessionObject(authorization); + if (sessionJson == null || sessionJson.isEmpty()) { + throw new IEMRException("Session expired. Please log in again."); + } + JSONObject session = new JSONObject(sessionJson); + return session.getLong("userID"); + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Authentication failed while extracting user ID: {}", e.getMessage()); + throw new IEMRException("Authentication failed"); + } + } + + private void validateAdminPrivileges(Long userId) throws IEMRException { + if (!iemrAdminUserServiceImpl.hasAdminPrivileges(userId)) { + logger.warn("Unauthorized access attempt by userId: {}", userId); + throw new IEMRException("Access denied. Admin privileges required."); + } + } } diff --git a/src/main/java/com/iemr/common/controller/version/VersionController.java b/src/main/java/com/iemr/common/controller/version/VersionController.java index 1b02ee59..69b067db 100644 --- a/src/main/java/com/iemr/common/controller/version/VersionController.java +++ b/src/main/java/com/iemr/common/controller/version/VersionController.java @@ -75,6 +75,8 @@ public ResponseEntity> versionInformation() { return ResponseEntity.ok(response); } + + private Properties loadGitProperties() throws IOException { Properties properties = new Properties(); try (InputStream input = getClass().getClassLoader() diff --git a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java index 8eb2a3ad..436b80a9 100644 --- a/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java +++ b/src/main/java/com/iemr/common/controller/videocall/VideoCallController.java @@ -1,5 +1,28 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.controller.videocall; +import java.net.URI; import java.util.HashMap; import java.util.Map; @@ -12,19 +35,19 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.service.videocall.VideoCallService; import com.iemr.common.utils.response.OutputResponse; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.web.bind.annotation.RequestBody; +import jakarta.servlet.http.HttpServletRequest; @RestController @RequestMapping(value = "/video-consultation") @@ -67,15 +90,24 @@ public String sendVideoLink(@RequestBody String requestModel, HttpServletRequest } @PostMapping(value = "/update-call-status", produces = MediaType.APPLICATION_JSON_VALUE, headers = "Authorization") -public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest requestModel, HttpServletRequest request) { +public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest requestModel, + HttpServletRequest request) { OutputResponse response = new OutputResponse(); + logger.info("[updateCallStatus CONTROLLER] Received — meetingLink={}, callStatus={}, callDuration={}, modifiedBy={}, isLinkUsed={}", + requestModel.getMeetingLink(), + requestModel.getCallStatus(), + requestModel.getCallDuration(), + requestModel.getModifiedBy(), + requestModel.getIsLinkUsed()); try { if (requestModel.getMeetingLink() == null || requestModel.getCallStatus() == null) { + logger.error("[updateCallStatus CONTROLLER] Validation failed — meetingLink or callStatus is null"); throw new IllegalArgumentException("Meeting Link and Status are required"); } String result = videoCallService.updateCallStatus(requestModel); + logger.info("[updateCallStatus CONTROLLER] Service returned successfully"); JSONObject responseObj = new JSONObject(); responseObj.put("status", "success"); @@ -83,16 +115,93 @@ public ResponseEntity updateCallStatus(@RequestBody UpdateCallRequest re response.setResponse(responseObj.toString()); } catch (IllegalArgumentException e) { - logger.error("Validation error: " + e.getMessage(), e); - return ResponseEntity.badRequest().body("{\"status\":\"error\",\"message\":\"" + e.getMessage() + "\"}"); + logger.error("[updateCallStatus CONTROLLER] Validation error: {}", e.getMessage(), e); + return ResponseEntity.badRequest() + .body("{\"status\":\"error\",\"message\":\"" + e.getMessage() + "\"}"); } catch (Exception e) { - logger.error("updateCallStatus failed with error: " + e.getMessage(), e); + logger.error("[updateCallStatus CONTROLLER] Unexpected error: {}", e.getMessage(), e); response.setError(e); } return ResponseEntity.ok(response.toString()); } +/** + * Returns a moderator JWT URL for the agent so they can use "End Meeting for All". + * Called by the frontend after the meeting link is generated. + */ +@PostMapping(value = "/agent-token", produces = MediaType.APPLICATION_JSON_VALUE, headers = "Authorization") +public ResponseEntity> generateAgentToken(@RequestBody Map body) { + Map response = new HashMap<>(); + try { + String slug = body.get("slug"); + String agentName = body.get("agentName"); + String agentEmail = body.get("agentEmail"); + + if (slug == null || slug.isEmpty()) { + response.put("error", "slug is required"); + return ResponseEntity.badRequest().body(response); + } + + String agentUrl = videoCallService.generateAgentToken(slug, agentName, agentEmail); + response.put("agentMeetingUrl", agentUrl); + + // Parse roomName and jwt out of the URL so the frontend can pass them + // directly to JitsiMeetExternalAPI without re-parsing the URL itself. + // URL format: https:///?jwt= + int jwtIdx = agentUrl.lastIndexOf("?jwt="); + if (jwtIdx > 0) { + String jwt = agentUrl.substring(jwtIdx + 5); + String pathPart = agentUrl.substring(0, jwtIdx); + String roomName = pathPart.substring(pathPart.lastIndexOf('/') + 1); + response.put("roomName", roomName); + response.put("jwt", jwt); + } + + return ResponseEntity.ok(response); + } catch (IllegalArgumentException e) { + response.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(response); + } catch (Exception e) { + logger.error("generateAgentToken failed: {}", e.getMessage(), e); + response.put("error", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } +} + +/** + * Public redirect endpoint hit when a beneficiary clicks the short SMS link. + * + * Flow: + * 1. Jitsi host nginx receives "https://vc.piramalswasthya.org/?m=<slug>" + * and proxies/redirects it to this endpoint. + * 2. This endpoint looks up the slug, mints a fresh Jitsi JWT bound to the + * room and the agent, and 302-redirects the browser to the full Jitsi URL + * "https://vc.piramalswasthya.org/<room>?jwt=<token>". + * 3. The Jitsi server enforces the JWT (prosody token-auth) and admits the user. + * + * Intentionally NOT guarded by Authorization header - the SMS recipient is on + * a phone browser and has no app session. Access control is the JWT itself + * plus the slug being unguessable and the meeting row existing. + */ +@GetMapping(value = "/resolve") +public ResponseEntity resolveMeetingLink(@RequestParam("m") String slug) { + try { + String redirectUrl = videoCallService.resolveMeetingLink(slug); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(redirectUrl)) + .build(); + } catch (IllegalArgumentException e) { + logger.warn("resolveMeetingLink rejected: {}", e.getMessage()); + return ResponseEntity.badRequest().build(); + } catch (Exception e) { + logger.error("resolveMeetingLink failed for slug={}: {}", slug, e.getMessage(), e); - + // Distinguish "link expired" from "not found" with a 410 Gone + if (e.getMessage() != null && e.getMessage().contains("already been used")) { + return ResponseEntity.status(HttpStatus.GONE).build(); // 410 + } + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); +} +} } diff --git a/src/main/java/com/iemr/common/data/translation/Translation.java b/src/main/java/com/iemr/common/data/translation/Translation.java index e0104a53..949ad20e 100644 --- a/src/main/java/com/iemr/common/data/translation/Translation.java +++ b/src/main/java/com/iemr/common/data/translation/Translation.java @@ -24,4 +24,5 @@ public class Translation { private String bengaliTranslation; @Column(name = "is_active") private Boolean isActive; + } diff --git a/src/main/java/com/iemr/common/data/users/User.java b/src/main/java/com/iemr/common/data/users/User.java index 4a9c6158..83e5ac27 100644 --- a/src/main/java/com/iemr/common/data/users/User.java +++ b/src/main/java/com/iemr/common/data/users/User.java @@ -216,6 +216,10 @@ public class User implements Serializable { @Column(name = "dhistoken") private String dhistoken; + @Expose + @Column(name = "lock_timestamp") + private Timestamp lockTimestamp; + /* * protected User() { } */ diff --git a/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java b/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java index c9df2d87..a852be81 100644 --- a/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java +++ b/src/main/java/com/iemr/common/data/videocall/VideoCallParameters.java @@ -1,3 +1,25 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.data.videocall; import java.sql.Timestamp; @@ -57,6 +79,9 @@ public class VideoCallParameters { @Column(name = "IsLinkUsed") private boolean linkUsed; + @Column(name = "RecordingFileName") + private String recordingFileName; + @Column(name = "Deleted", insertable = false, updatable = true) private Boolean deleted; diff --git a/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java b/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java index 521d5921..7e9a8f12 100644 --- a/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java +++ b/src/main/java/com/iemr/common/mapper/videocall/VideoCallMapper.java @@ -1,8 +1,29 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.mapper.videocall; import java.util.List; import org.mapstruct.Mapper; -import org.mapstruct.factory.Mappers; import org.mapstruct.IterableMapping; import org.mapstruct.factory.Mappers; diff --git a/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java b/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java index e7a7a3de..5d42f275 100644 --- a/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java +++ b/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryModel.java @@ -118,7 +118,10 @@ public class BeneficiaryModel implements Comparable { private Boolean isMarried; @Expose - private Integer doYouHavechildren; + private boolean doYouHavechildren; + + @Expose + private Integer noOfchildren; @Expose private Integer noofAlivechildren; diff --git a/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java b/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java index 343198b3..39cc4e13 100644 --- a/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java +++ b/src/main/java/com/iemr/common/model/videocall/UpdateCallRequest.java @@ -1,12 +1,35 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.model.videocall; import lombok.Data; @Data public class UpdateCallRequest { - + private String meetingLink; - private String callStatus; - private String callDuration; + private String callStatus; + private String callDuration; private String modifiedBy; + private Boolean isLinkUsed; } diff --git a/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java b/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java index f01f46f5..8843f887 100644 --- a/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java +++ b/src/main/java/com/iemr/common/model/videocall/UpdateCallResponse.java @@ -1,3 +1,25 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.model.videocall; import java.sql.Timestamp; @@ -13,6 +35,7 @@ public class UpdateCallResponse { private String callDuration; private String modifiedBy; private boolean isLinkUsed; + private String recordingFileName; @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") private Timestamp lastModified; diff --git a/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java b/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java index d8a61eee..64abc044 100644 --- a/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java +++ b/src/main/java/com/iemr/common/model/videocall/VideoCallRequest.java @@ -1,3 +1,25 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.model.videocall; import com.fasterxml.jackson.annotation.JsonFormat; diff --git a/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java b/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java index 4aea5698..50e84248 100644 --- a/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java +++ b/src/main/java/com/iemr/common/repository/dynamic_form/FieldRepository.java @@ -9,4 +9,10 @@ @Repository public interface FieldRepository extends JpaRepository { List findByForm_FormIdOrderBySequenceAsc(String formId); + List findByForm_FormIdAndStateCodeOrderBySequenceAsc( + String formId, + Integer stateCode + ); + + } diff --git a/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java b/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java index f6a5dcb0..139b5ee9 100644 --- a/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java +++ b/src/main/java/com/iemr/common/repository/translation/TranslationRepo.java @@ -10,4 +10,5 @@ public interface TranslationRepo extends JpaRepository { Optional findByLabelKeyAndIsActive(String labelKey, boolean isActive); + } diff --git a/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java b/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java index 5623448d..07100ca4 100644 --- a/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java +++ b/src/main/java/com/iemr/common/repository/users/FacilityLoginRepo.java @@ -55,7 +55,8 @@ public interface FacilityLoginRepo extends CrudRepository getPeersAtFacility(@Param("facilityIDs") List facilityID @Query(value = "SELECT DISTINCT u.UserID, u.FirstName, u.LastName, " + "COALESCE(u.EmployeeID,'') AS employeeID, " + "f.FacilityID, f.FacilityName, " - + "COALESCE(ft.FacilityTypeName,'') AS facilityTypeName " + + "COALESCE(ft.FacilityTypeName,'') AS facilityTypeName, " + + "COALESCE(u.ContactNo,'') AS mobile " + "FROM asha_supervisor_mapping asm " + "JOIN m_User u ON u.UserID = asm.ashaUserID " + "JOIN m_facility f ON f.FacilityID = asm.facilityID " @@ -82,7 +84,8 @@ List getPeersAtFacility(@Param("facilityIDs") List facilityID @Query(value = "SELECT DISTINCT u.UserID, u.FirstName, u.LastName, " + "COALESCE(u.EmployeeID,'') AS employeeID, " + "f.FacilityID, f.FacilityName, " - + "COALESCE(ft.FacilityTypeName,'') AS facilityTypeName " + + "COALESCE(ft.FacilityTypeName,'') AS facilityTypeName, " + + "COALESCE(u.ContactNo,'') AS mobile " + "FROM m_UserServiceRoleMapping usrm " + "JOIN m_User u ON u.UserID = usrm.UserID " + "JOIN m_Role r ON r.RoleID = usrm.RoleID " diff --git a/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java b/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java index cc1abccc..4a2bef76 100644 --- a/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java +++ b/src/main/java/com/iemr/common/repository/users/IEMRUserRepositoryCustom.java @@ -75,7 +75,7 @@ UserSecurityQMapping verifySecurityQuestionAnswers(@Param("UserID") Long UserID, @Query("SELECT u FROM User u WHERE u.userID=5718") User getAllExistingUsers(); - + User findByUserID(Long userID); @Query("SELECT u FROM User u WHERE LOWER(u.userName) = LOWER(:userName)") diff --git a/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java b/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java index 251b877a..7c9cbf26 100644 --- a/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java +++ b/src/main/java/com/iemr/common/repository/videocall/VideoCallParameterRepository.java @@ -1,17 +1,35 @@ -package com.iemr.common.repository.videocall; +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ -import java.util.List; +package com.iemr.common.repository.videocall; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.data.jpa.repository.Query; import com.iemr.common.data.videocall.VideoCallParameters; -import com.iemr.common.model.videocall.VideoCallRequest; import org.springframework.data.jpa.repository.Modifying; import org.springframework.transaction.annotation.Transactional; - @Repository public interface VideoCallParameterRepository extends CrudRepository { @@ -19,14 +37,28 @@ public interface VideoCallParameterRepository extends CrudRepository callAgentSummaryReportCTI_API() throws IEMRExcep // throw new IEMRException("Please pass correct period for schedular - in hours"); String ctiURI = ConfigProperties.getPropertyByName("get-agent-summary-report-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("END_DATE", endDate); ctiURI = ctiURI.replace("START_DATE", fromDate); @@ -272,7 +276,7 @@ public List callDetailedCallReportCTI_API() throws IEMRExcep // throw new IEMRException("Please pass correct period for schedular - in hours"); String ctiURI = ConfigProperties.getPropertyByName("get-details-call-report-URL"); - String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); + // String serverURL = ConfigProperties.getPropertyByName("cti-server-ip"); ctiURI = ctiURI.replace("CTI_SERVER", serverURL); ctiURI = ctiURI.replace("END_DATE", endDate); ctiURI = ctiURI.replace("START_DATE", fromDate); diff --git a/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java b/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java index 2d8aaeb3..a4fda6f5 100644 --- a/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java +++ b/src/main/java/com/iemr/common/service/notification/NotificationServiceImpl.java @@ -38,6 +38,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonProcessingException; @@ -70,6 +71,19 @@ public class NotificationServiceImpl implements NotificationService private EmailService emailService; + + @Value("${km-base-path}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + + @Value("${km-guest-password}") + private String userPassword; + + @Value("${km-base-protocol}") + private String dmsProtocol; + @Autowired public void setEmailService(EmailService emailService) { @@ -415,10 +429,7 @@ private String getFilePath(KMFileManager kmFileManager) if (kmFileManager != null && kmFileManager.getFileUID() != null) { String fileUID = kmFileManager.getFileUID(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); + fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; } diff --git a/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java b/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java index 44e1efaa..d119a85b 100644 --- a/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java +++ b/src/main/java/com/iemr/common/service/scheme/SchemeServiceImpl.java @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -59,6 +60,18 @@ public class SchemeServiceImpl implements SchemeService { private KMFileManagerService kmFileManagerService; + @Value("${km-base-protocol}") + private String dmsProtocol; + + @Value("${km-base-url}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + + @Value("${km-guest-password}") + private String userPassword; + @Autowired public void setKmFileManagerService(KMFileManagerService kmFileManagerService) { this.kmFileManagerService = kmFileManagerService; @@ -104,16 +117,13 @@ public String getFilePath(KMFileManager kmFileManager) { String fileUIDAsURI = null; if (kmFileManager != null && kmFileManager.getFileUID() != null) { String fileUID = kmFileManager.getFileUID(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); + fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; } - // return fileUIDAsURI; - String message = kmFileManager.getFileUID() ; - return message; + return fileUIDAsURI; + // String message = kmFileManager.getFileUID() ; + // return message; } @Override diff --git a/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java b/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java index ff6f83e9..d8587a86 100644 --- a/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java +++ b/src/main/java/com/iemr/common/service/services/CommonServiceImpl.java @@ -64,7 +64,19 @@ public class CommonServiceImpl implements CommonService { private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + @Value("${km-base-path}") + private String dmsPath; + + @Value("${km-guest-user}") + private String userName; + @Value("${km-guest-password}") + private String userPassword; + + @Value("${km-base-protocol}") + private String dmsProtocol; + private static final String FILE_PATH = "filePath"; /** @@ -177,10 +189,6 @@ private String getFilePath(String fileUID) { String fileUIDAsURI = null; - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + fileUID; @@ -233,12 +241,13 @@ public List getSubCategoryFilesWithURL(String request) throw SubCategoryDetails subCategory = subCategoriesList.get(index); if (subCategory.getSubCatFilePath() != null && subCategory.getSubCatFilePath().length() > 0) { String subCatFilePath = subCategory.getSubCatFilePath(); - String dmsPath = ConfigProperties.getPropertyByName("km-base-path"); - String dmsProtocol = ConfigProperties.getPropertyByName("km-base-protocol"); - String userName = ConfigProperties.getPropertyByName("km-guest-user"); - String userPassword = ConfigProperties.getPassword("km-guest-user"); String fileUIDAsURI = dmsProtocol + "://" + userName + ":" + userPassword + "@" + dmsPath + "/Download?uuid=" + subCategory.getSubCatFilePath(); + logger.info("file url="+fileUIDAsURI); + logger.info("file path="+subCategory.getSubCatFilePath()); + logger.info("dms Path="+dmsPath); + logger.info("subcatfilePath="+subCatFilePath); + subCategory.setSubCatFilePath(fileUIDAsURI); subCategoriesList.get(index).setFileManger(kmFileManagerRepository .getKMFileLists(subCategoryDetails.getProviderServiceMapID(), subCatFilePath)); diff --git a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java index efe0d16a..c49eca10 100644 --- a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java +++ b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java @@ -391,8 +391,8 @@ public SMSNotification prepareVideoCallSMS(SMSRequest request, VideoCallParamete String variable = smsParametersMap.getSmsParameterName(); String methodName = smsParametersMap.getSmsParameter().getDataName(); String variableValue = ""; - variableValue = getVideoCallData(methodName, vcParams); - smsToSend = smsToSend.replace("$$" + variable + "$$", variableValue); + // variableValue = getVideoCallData(methodName, vcParams); + // smsToSend = smsToSend.replace("$$" + variable + "$$", variableValue); if ("VideoCall".equalsIgnoreCase(smsParametersMap.getSmsParameter().getSmsParameterType())) { variableValue = getVideoCallData(methodName, vcParams); @@ -436,10 +436,15 @@ public String getVideoCallData(String methodName, VideoCallParameters videoCall) variableValue = videoCall.getCallerPhoneNumber() !=null ? videoCall.getCallerPhoneNumber().toString() : ""; break; default: - Method method = videoCall.getClass().getDeclaredMethod("get" + capitalize(methodName)); - method.setAccessible(true); - Object result = method.invoke(videoCall); - variableValue = result != null ? result.toString() : ""; + try { + Method method = videoCall.getClass().getDeclaredMethod("get" + capitalize(methodName)); + method.setAccessible(true); + Object result = method.invoke(videoCall); + variableValue = result != null ? result.toString() : ""; + } catch (NoSuchMethodException e) { + logger.warn("No getter found for methodName: " + methodName + " on VideoCallParameters"); + variableValue = ""; + } break; } return variableValue.trim(); @@ -678,7 +683,7 @@ private SMSNotification prepareSMS( sms.setReceivingUserID(request.getUserID()); String smsToSend = ""; BeneficiaryModel beneficiary = null; - if (request.getBeneficiaryRegID() != null) { + if (request.getBeneficiaryRegID() != null && !request.getBeneficiaryRegID().toString().isEmpty()) { List beneficiaries = searchBeneficiary.userExitsCheckWithId(request.getBeneficiaryRegID(), authToken, request.getIs1097()); if (beneficiaries.size() == 1) @@ -844,6 +849,12 @@ private String getUserData(String className, String methodName, SMSRequest reque private String getBeneficiaryData(String className, String methodName, SMSRequest request, BeneficiaryModel beneficiary) throws Exception { String variableValue = ""; + if (beneficiary == null) { + if ("phoneno".equalsIgnoreCase(methodName)) { + return request.getBenPhoneNo() != null ? request.getBenPhoneNo() : ""; + } + return ""; + } switch (methodName.toLowerCase()) { case "name": String fname = beneficiary.getFirstName() != null ? beneficiary.getFirstName() + " " : ""; @@ -875,9 +886,16 @@ private String getBeneficiaryData(String className, String methodName, SMSReques variableValue = imrName; break; default: - Class clazz = Class.forName(className); - Method method = clazz.getDeclaredMethod("get" + methodName, null); - variableValue = method.invoke(beneficiary, null).toString(); + if ("com.iemr.common.data.videocall.VideoCallParameters".equals(className)) { + VideoCallParameters vcParams = getVideoCallParameters(request.getSmsAdvice()); + if (vcParams != null) { + variableValue = getVideoCallData(methodName, vcParams); + } + } else { + Class clazz = Class.forName(className); + Method method = clazz.getDeclaredMethod("get" + methodName, null); + variableValue = method.invoke(beneficiary, null).toString(); + } break; } diff --git a/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java b/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java index d4ce1420..a3fc6728 100644 --- a/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java +++ b/src/main/java/com/iemr/common/service/users/AshaSupervisorLoginService.java @@ -102,6 +102,7 @@ private void enrichAshaData(JSONObject result, Integer ashaUserID) { peer.put("fullName", fullName(pRow[1], pRow[2])); peer.put("role", str(pRow[3])); peer.put("employeeId", str(pRow[4]).isEmpty() ? JSONObject.NULL : str(pRow[4])); + peer.put("mobile", str(pRow[5]).isEmpty() ? JSONObject.NULL : str(pRow[5])); peers.put(peer); } } @@ -193,6 +194,7 @@ private JSONArray buildAshaList(List rows) { asha.put("facilityId", row[4]); asha.put("facilityName", str(row[5])); asha.put("facilityType", str(row[6])); + asha.put("mobile", str(row[7]).isEmpty() ? JSONObject.NULL : str(row[7])); list.put(asha); } return list; diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java index 1db05322..cbd29cf1 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java @@ -125,6 +125,13 @@ public List getUserServiceRoleMappingForProvider(Integ List findUserIdByUserName(String userName) throws IEMRException; + boolean lockUserAccount(Long userId) throws IEMRException; + + boolean unlockUserAccount(Long userId) throws IEMRException; + + String getUserLockStatusJson(Long userId) throws IEMRException; + + boolean hasAdminPrivileges(Long userId) throws IEMRException; User save(User loggedInUser); } diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java index 13adda14..d8d4abe8 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -129,6 +129,8 @@ public class IEMRAdminUserServiceImpl implements IEMRAdminUserService { private SessionObject sessionObject; @Value("${failedLoginAttempt}") private String failedLoginAttempt; + @Value("${account.lock.duration.hours:24}") + private int accountLockDurationHours; // @Autowired // private ServiceRoleScreenMappingRepository ; @@ -221,95 +223,121 @@ public void setValidator(Validator validator) { } private void checkUserAccountStatus(User user) throws IEMRException { - if (user.getDeleted()) { - throw new IEMRException("Your account is locked or de-activated. Please contact administrator"); + if (user.getDeleted() != null && user.getDeleted()) { + if (user.getLockTimestamp() != null) { + long lockTimeMillis = user.getLockTimestamp().getTime(); + long currentTimeMillis = System.currentTimeMillis(); + long lockDurationMillis = getLockDurationMillis(); + + if (currentTimeMillis - lockTimeMillis >= lockDurationMillis) { + user.setDeleted(false); + user.setFailedAttempt(0); + user.setLockTimestamp(null); + iEMRUserRepositoryCustom.save(user); + logger.info("User account auto-unlocked after {} hours lock period for user: {}", + accountLockDurationHours, user.getUserName()); + } else { + throw new IEMRException(generateLockoutErrorMessage(user.getLockTimestamp())); + } + } else { + throw new IEMRException("Your account is locked or de-activated. Please contact administrator"); + } } else if (user.getStatusID() > 2) { throw new IEMRException("Your account is not active. Please contact administrator"); } } - @Override - public List userAuthenticate(String userName, String password) throws Exception { - List users = iEMRUserRepositoryCustom.findByUserNameNew(userName); - if (users.size() != 1) { - throw new IEMRException("Invalid username or password"); - } - int failedAttempt = 0; - if (failedLoginAttempt != null) - failedAttempt = Integer.parseInt(failedLoginAttempt); - else - failedAttempt = 5; - User user = users.get(0); - try { - int validatePassword; - validatePassword = securePassword.validatePassword(password, user.getPassword()); - if (validatePassword == 1) { + /** + * Common helper method for password validation and account locking logic. + * Used by both userAuthenticate() and superUserAuthenticate(). + */ + private User handlePasswordValidationAndLocking(User user, String password, int failedAttemptThreshold) + throws IEMRException, NoSuchAlgorithmException, InvalidKeySpecException { + int validatePassword = securePassword.validatePassword(password, user.getPassword()); + + switch (validatePassword) { + case 0: + // Invalid password - handle failed attempts + handleFailedLoginAttempt(user, failedAttemptThreshold); + break; + case 1: + // Valid password with old format - upgrade to new format checkUserAccountStatus(user); - int iterations = 1001; - char[] chars = password.toCharArray(); - byte[] salt = getSalt(); - - PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 512); - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); - byte[] hash = skf.generateSecret(spec).getEncoded(); - String updatedPassword = iterations + ":" + toHex(salt) + ":" + toHex(hash); - // save operation - user.setPassword(updatedPassword); + clearFailedAttemptState(user); + user.setPassword(generateUpgradedPassword(password)); iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 2) { + break; + case 2, 3: + // Valid password checkUserAccountStatus(user); + clearFailedAttemptState(user); iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 3) { + break; + default: + // Successful validation - reset failed attempts if needed checkUserAccountStatus(user); - iEMRUserRepositoryCustom.save(user); - } else if (validatePassword == 0) { - int currentFailedAttempt = - user.getFailedAttempt() != null ? user.getFailedAttempt() : 0; + resetFailedAttemptsIfNeeded(user); + break; + } + return user; + } - int newFailedAttempt = currentFailedAttempt + 1; - int remainingAttempts = failedAttempt - newFailedAttempt; - if (newFailedAttempt < failedAttempt) { + private void clearFailedAttemptState(User user) { + user.setFailedAttempt(0); + user.setLockTimestamp(null); + } - user.setFailedAttempt(newFailedAttempt); - user = iEMRUserRepositoryCustom.save(user); + private long getLockDurationMillis() { + return (long) accountLockDurationHours * 60 * 60 * 1000; + } - logger.warn("User Password Wrong"); + private String generateUpgradedPassword(String password) + throws NoSuchAlgorithmException, InvalidKeySpecException { + int iterations = 1001; + char[] chars = password.toCharArray(); + byte[] salt = getSalt(); + PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 512); + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); + byte[] hash = skf.generateSecret(spec).getEncoded(); + return iterations + ":" + toHex(salt) + ":" + toHex(hash); + } - if (remainingAttempts == 1) { - throw new IEMRException( - "Invalid username or password. Remaining attempts: 1. " - + "If you enter wrong username or password again, your account will be locked."); - } + private void handleFailedLoginAttempt(User user, int failedAttemptThreshold) throws IEMRException { + int currentAttempts = (user.getFailedAttempt() != null) ? user.getFailedAttempt() : 0; + if (currentAttempts + 1 < failedAttemptThreshold) { + user.setFailedAttempt(currentAttempts + 1); + iEMRUserRepositoryCustom.save(user); + logger.warn("User Password Wrong"); + throw new IEMRException("Invalid username or password"); + } else { + java.sql.Timestamp lockTime = new java.sql.Timestamp(System.currentTimeMillis()); + user.setFailedAttempt(currentAttempts + 1); + user.setDeleted(true); + user.setLockTimestamp(lockTime); + iEMRUserRepositoryCustom.save(user); + logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", + failedAttemptThreshold); + throw new IEMRException(generateLockoutErrorMessage(lockTime)); + } + } - throw new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts); - }else if (user.getFailedAttempt() + 1 >= failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user.setDeleted(true); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", - ConfigProperties.getInteger("failedLoginAttempt")); - - throw new IEMRException( - "Your account has been locked due to multiple failed login attempts. Please contact administrator."); - } else { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("Failed login attempt {} of {} for a user account.", - user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt")); - throw new IEMRException( - "Your account has been locked due to multiple failed login attempts. Please contact administrator."); - } - } else { - checkUserAccountStatus(user); - if (user.getFailedAttempt() != 0) { - user.setFailedAttempt(0); - user = iEMRUserRepositoryCustom.save(user); - } - } + private void resetFailedAttemptsIfNeeded(User user) { + if (user.getFailedAttempt() != null && user.getFailedAttempt() != 0) { + clearFailedAttemptState(user); + iEMRUserRepositoryCustom.save(user); + } + } + + @Override + public List userAuthenticate(String userName, String password) throws Exception { + List users = iEMRUserRepositoryCustom.findByUserNameNew(userName); + if (users.size() != 1) { + throw new IEMRException("Invalid username or password"); + } + int failedAttemptThreshold = getFailedAttemptThreshold(); + User user = users.get(0); + try { + handlePasswordValidationAndLocking(user, password, failedAttemptThreshold); } catch (Exception e) { throw new IEMRException(e.getMessage()); } @@ -317,16 +345,36 @@ public List userAuthenticate(String userName, String password) throws Exce return users; } - private void checkUserLoginFailedAttempt(User user) throws IEMRException { - + private int getFailedAttemptThreshold() { + if (failedLoginAttempt != null && !failedLoginAttempt.trim().isEmpty()) { + try { + return Integer.parseInt(failedLoginAttempt.trim()); + } catch (NumberFormatException e) { + logger.warn("Invalid failedLoginAttempt configuration value '{}', using default of 5", failedLoginAttempt); + } + } + return 5; } - private void updateUserLoginFailedAttempt(User user) throws IEMRException { + private String generateLockoutErrorMessage(java.sql.Timestamp lockTimestamp) { + if (lockTimestamp == null) { + return "Your account has been locked. Please contact the administrator."; + } - } + long remainingMillis = calculateRemainingLockTime(lockTimestamp); - private void resetUserLoginFailedAttempt(User user) throws IEMRException { + if (remainingMillis <= 0) { + return "Your account lock has expired. Please try logging in again."; + } + return "Your account has been locked. You can try tomorrow or connect to the administrator."; + } + + private long calculateRemainingLockTime(java.sql.Timestamp lockTimestamp) { + long lockTimeMillis = lockTimestamp.getTime(); + long currentTimeMillis = System.currentTimeMillis(); + long unlockTimeMillis = lockTimeMillis + getLockDurationMillis(); + return unlockTimeMillis - currentTimeMillis; } /** @@ -335,67 +383,13 @@ private void resetUserLoginFailedAttempt(User user) throws IEMRException { @Override public User superUserAuthenticate(String userName, String password) throws Exception { List users = iEMRUserRepositoryCustom.findByUserName(userName); - if (users.size() != 1) { throw new IEMRException("Invalid username or password"); } - int failedAttempt = 0; - if (failedLoginAttempt != null) - failedAttempt = Integer.parseInt(failedLoginAttempt); - else - failedAttempt = 5; + int failedAttemptThreshold = getFailedAttemptThreshold(); User user = users.get(0); try { - int validatePassword; - validatePassword = securePassword.validatePassword(password, user.getPassword()); - if (validatePassword == 1) { - checkUserAccountStatus(user); - int iterations = 1001; - char[] chars = password.toCharArray(); - byte[] salt = getSalt(); - - PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 512); - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); - byte[] hash = skf.generateSecret(spec).getEncoded(); - String updatedPassword = iterations + ":" + toHex(salt) + ":" + toHex(hash); - // save operation - user.setPassword(updatedPassword); - iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 2) { - checkUserAccountStatus(user); - iEMRUserRepositoryCustom.save(user); - - } else if (validatePassword == 0) { - if (user.getFailedAttempt() + 1 < failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); - } else if (user.getFailedAttempt() + 1 >= failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user.setDeleted(true); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", - ConfigProperties.getInteger("failedLoginAttempt")); - - throw new IEMRException( - "Invalid username or password. Please contact administrator."); - } else { - user.setFailedAttempt(user.getFailedAttempt() + 1); - user = iEMRUserRepositoryCustom.save(user); - logger.warn("Failed login attempt {} of {} for a user account.", - user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt")); - throw new IEMRException( - "Invalid username or password. Please contact administrator."); - } - } else { - checkUserAccountStatus(user); - if (user.getFailedAttempt() != 0) { - user.setFailedAttempt(0); - user = iEMRUserRepositoryCustom.save(user); - } - } + handlePasswordValidationAndLocking(user, password, failedAttemptThreshold); } catch (Exception e) { throw new IEMRException(e.getMessage()); } @@ -1221,12 +1215,12 @@ public User getUserById(Long userId) throws IEMRException { try { // Fetch user from custom repository by userId User user = iEMRUserRepositoryCustom.findByUserID(userId); - + // Check if user is found if (user == null) { throw new IEMRException("User not found with ID: " + userId); } - + return user; } catch (Exception e) { // Log and throw custom exception in case of errors @@ -1237,16 +1231,153 @@ public User getUserById(Long userId) throws IEMRException { @Override public List getUserIdbyUserName(String userName) { - return iEMRUserRepositoryCustom.findByUserName(userName); } - @Override + @Override public List findUserIdByUserName(String userName) { - return iEMRUserRepositoryCustom.findUserName(userName); } + @Override + public boolean lockUserAccount(Long userId) throws IEMRException { + try { + User user = iEMRUserRepositoryCustom.findById(userId).orElse(null); + + if (user == null) { + throw new IEMRException("User not found with ID: " + userId); + } + + if (user.getDeleted() != null && user.getDeleted()) { + if (user.getLockTimestamp() == null) { + throw new IEMRException("User account is deactivated by administrator. Activate the account before locking it."); + } + logger.info("User account is already locked for userID: {}", userId); + return false; + } + + user.setDeleted(true); + user.setFailedAttempt(getFailedAttemptThreshold()); + user.setLockTimestamp(new java.sql.Timestamp(System.currentTimeMillis())); + iEMRUserRepositoryCustom.save(user); + logger.info("Admin manually locked user account for userID: {}", userId); + return true; + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error locking user account with ID: " + userId, e); + throw new IEMRException("Error locking user account: " + e.getMessage(), e); + } + } + + @Override + public boolean unlockUserAccount(Long userId) throws IEMRException { + try { + User user = iEMRUserRepositoryCustom.findById(userId).orElse(null); + + if (user == null) { + throw new IEMRException("User not found with ID: " + userId); + } + + if (user.getDeleted() != null && user.getDeleted() && user.getLockTimestamp() != null) { + user.setDeleted(false); + user.setFailedAttempt(0); + user.setLockTimestamp(null); + iEMRUserRepositoryCustom.save(user); + logger.info("Admin manually unlocked user account for userID: {}", userId); + return true; + } else if (user.getDeleted() != null && user.getDeleted() && user.getLockTimestamp() == null) { + throw new IEMRException("User account is deactivated by administrator. Use user management to reactivate."); + } else { + logger.info("User account is not locked for userID: {}", userId); + return false; + } + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error unlocking user account with ID: " + userId, e); + throw new IEMRException("Error unlocking user account: " + e.getMessage(), e); + } + } + + @Override + public String getUserLockStatusJson(Long userId) throws IEMRException { + try { + User user = iEMRUserRepositoryCustom.findById(userId).orElse(null); + if (user == null) { + throw new IEMRException("User not found with ID: " + userId); + } + + org.json.JSONObject status = new org.json.JSONObject(); + status.put("userId", user.getUserID()); + status.put("userName", user.getUserName()); + status.put("failedAttempts", user.getFailedAttempt() != null ? user.getFailedAttempt() : 0); + status.put("statusID", user.getStatusID()); + + boolean isDeleted = user.getDeleted() != null && user.getDeleted(); + boolean isLockedDueToFailedAttempts = isDeleted && user.getLockTimestamp() != null; + + status.put("isLocked", isDeleted); + status.put("isLockedDueToFailedAttempts", isLockedDueToFailedAttempts); + + if (isLockedDueToFailedAttempts) { + long remainingMillis = calculateRemainingLockTime(user.getLockTimestamp()); + boolean lockExpired = remainingMillis <= 0; + + status.put("lockExpired", lockExpired); + status.put("lockTimestamp", user.getLockTimestamp().toString()); + status.put("remainingTime", lockExpired ? "Lock expired - will unlock on next login" : formatRemainingTime(remainingMillis)); + if (!lockExpired) { + status.put("unlockTime", new java.sql.Timestamp(user.getLockTimestamp().getTime() + getLockDurationMillis()).toString()); + } + } else { + status.put("lockExpired", false); + status.put("lockTimestamp", org.json.JSONObject.NULL); + status.put("remainingTime", org.json.JSONObject.NULL); + } + + return status.toString(); + } catch (IEMRException e) { + throw e; + } catch (Exception e) { + logger.error("Error fetching user lock status with ID: " + userId, e); + throw new IEMRException("Error fetching user lock status: " + e.getMessage(), e); + } + } + + private String formatRemainingTime(long remainingMillis) { + long hours = remainingMillis / (60 * 60 * 1000); + long minutes = (remainingMillis % (60 * 60 * 1000)) / (60 * 1000); + if (hours > 0 && minutes > 0) return String.format("%d hours %d minutes", hours, minutes); + if (hours > 0) return String.format("%d hours", hours); + return String.format("%d minutes", minutes); + } + + private static final Set ADMIN_ROLE_NAMES = Set.of("admin", "supervisor", "provideradmin"); + + @Override + public boolean hasAdminPrivileges(Long userId) throws IEMRException { + try { + List roleMappings = getUserServiceRoleMapping(userId); + if (roleMappings == null || roleMappings.isEmpty()) { + return false; + } + for (UserServiceRoleMapping mapping : roleMappings) { + Role role = mapping.getM_Role(); + if (role != null && role.getRoleName() != null) { + String roleName = role.getRoleName().trim().toLowerCase(); + if (ADMIN_ROLE_NAMES.contains(roleName)) { + return true; + } + } + } + return false; + } catch (Exception e) { + logger.error("Error checking admin privileges for userId: " + userId, e); + return false; + } + } + @Override public User save(User loggedInUser) { return iEMRUserRepositoryCustom.save(loggedInUser); diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallService.java b/src/main/java/com/iemr/common/service/videocall/VideoCallService.java index 9322050b..81975d53 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallService.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallService.java @@ -1,13 +1,58 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.service.videocall; -import com.iemr.common.utils.response.OutputResponse; + import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; public interface VideoCallService { - + public String generateMeetingLink() throws Exception; public String sendMeetingLink(VideoCallRequest request) throws Exception; public String updateCallStatus(UpdateCallRequest request) throws Exception; + + /** + * Resolve the short slug carried in the SMS link (the value after "?m=") + * into the full Jitsi URL with a freshly minted JWT appended. + * Called by the public redirect endpoint that the Jitsi host's nginx + * proxies "/?m=<slug>" requests to. + * + * @param slug the random slug originally generated by {@link #generateMeetingLink()} + * @return absolute URL of the form + * https://<jitsi.domain>/<jitsi.room.prefix><slug>?jwt=<token> + */ + public String resolveMeetingLink(String slug) throws Exception; + + /** + * Generate a moderator JWT URL for the agent/associate so they can join + * the Jitsi room with "End Meeting for All" privileges. + * + * @param slug the meeting slug (value after "m=" in the meeting link) + * @param agentName display name for the agent in the Jitsi UI + * @param agentEmail agent email (used for Jitsi avatar / gravatar) + * @return absolute Jitsi URL with moderator JWT appended + */ + public String generateAgentToken(String slug, String agentName, String agentEmail) throws Exception; } diff --git a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java index 9db1a771..04518f32 100644 --- a/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java +++ b/src/main/java/com/iemr/common/service/videocall/VideoCallServiceImpl.java @@ -1,3 +1,25 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + package com.iemr.common.service.videocall; import org.apache.commons.lang.RandomStringUtils; @@ -7,18 +29,12 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.sql.Timestamp; -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.io.IOException; import com.iemr.common.data.videocall.VideoCallParameters; import com.iemr.common.mapper.videocall.VideoCallMapper; import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.repository.videocall.VideoCallParameterRepository; -import com.iemr.common.utils.config.ConfigProperties; +import com.iemr.common.utils.JitsiJwtUtil; import com.iemr.common.utils.mapper.OutputMapper; import com.iemr.common.utils.response.OutputResponse; import org.springframework.beans.factory.annotation.Value; @@ -29,28 +45,38 @@ public class VideoCallServiceImpl implements VideoCallService { @Autowired private VideoCallParameterRepository videoCallRepository; - + @Autowired private VideoCallMapper videoCallMapper; + @Autowired + private JitsiJwtUtil jitsiJwtUtil; + private String meetingLink; private boolean isLinkSent = false; - private String consultationStatus = "Not Initiated"; - @Value("${video-call-url}") + @Value("${videocall.url}") private String jitsiLink; + @Value("${jitsi.domain}") + private String jitsiDomain; + + @Value("${jitsi.room.prefix}") + private String roomPrefix; + + @Value("${jitsi.default.user.email}") + private String defaultUserEmail; + public VideoCallServiceImpl() { - // this.jitsiLink = ConfigProperties.getPropertyByName("video-call-url"); - // logger.info("Jitsi Link fetched: " + this.jitsiLink); + // Default constructor + this.meetingLink = null; + this.isLinkSent = false; } @Override public String generateMeetingLink() { - logger.info("Jitsi Link: " + jitsiLink); meetingLink=jitsiLink+"m="+RandomStringUtils.randomAlphanumeric(8); - logger.info("Meeting link: " + meetingLink); return meetingLink; } @@ -83,54 +109,117 @@ public String sendMeetingLink(VideoCallRequest request) throws Exception { @Override public String updateCallStatus(UpdateCallRequest callRequest) throws Exception { - VideoCallParameters videoCall = null; + String meetingLink = callRequest.getMeetingLink(); + + // 1. Verify the row actually exists before attempting update + VideoCallParameters existing = videoCallRepository.findByMeetingLink(meetingLink); + if (existing == null) { + logger.error("[updateCallStatus] No row found in t_videocallparameter for meetingLink={}", meetingLink); + throw new Exception("No meeting found for link: " + meetingLink); + } + + // 2. Derive the two fields + boolean linkUsed = callRequest.getIsLinkUsed() == null || callRequest.getIsLinkUsed(); + String recordingFileName = buildRecordingFileName(meetingLink); + + // 3. Single atomic JPQL UPDATE — sets ALL five fields in one DB round-trip + int updateCount = videoCallRepository.updateCallStatusAndRecording( + meetingLink, + callRequest.getCallStatus(), + callRequest.getCallDuration(), + callRequest.getModifiedBy(), + linkUsed, + recordingFileName + ); + logger.info("[updateCallStatus] JPQL updateCallStatusAndRecording affected {} row(s)", updateCount); - VideoCallParameters requestEntity = videoCallMapper.updateRequestToVideoCall(callRequest); + if (updateCount == 0) { + logger.error("[updateCallStatus] Update affected 0 rows — possible meetingLink mismatch. meetingLink={}", meetingLink); + throw new Exception("Failed to update the call status — 0 rows affected"); + } - videoCall = videoCallRepository.findByMeetingLink(requestEntity.getMeetingLink()); + // 4. Re-fetch AFTER the update so the returned JSON reflects what is now in the DB + VideoCallParameters updated = videoCallRepository.findByMeetingLink(meetingLink); + + return OutputMapper.gsonWithoutExposeRestriction() + .toJson(videoCallMapper.videoCallToResponse(updated)); +} - int updateCount = videoCallRepository.updateCallStatusByMeetingLink( - requestEntity.getMeetingLink(), - requestEntity.getCallStatus(), - requestEntity.getCallDuration(), - requestEntity.getModifiedBy() - ); +/** + * Jibri records each Jitsi room into a directory named after the room, with + * the MP4 file sharing the same name — e.g. piramal-meeting-Ab3xQ9pK/piramal-meeting-Ab3xQ9pK.mp4. + * The short SMS link is "m=", so derive the room from the slug. + */ +private String buildRecordingFileName(String meetingLink) { - if (updateCount > 0) { - videoCall.setLinkUsed(true); - videoCallRepository.save(videoCall); - - // if ("Completed".equalsIgnoreCase(requestEntity.getCallStatus())) { - // saveRecordingFile(videoCall.getMeetingLink()); - // } - } else { - throw new Exception("Failed to update the call status"); + if (meetingLink == null) { + logger.warn("[buildRecordingFileName] meetingLink is null — returning null"); + return null; } - return OutputMapper.gsonWithoutExposeRestriction() - .toJson(videoCallMapper.videoCallToResponse(videoCall)); + int idx = meetingLink.lastIndexOf("m="); + if (idx < 0) { + logger.warn("[buildRecordingFileName] 'm=' marker not found in meetingLink={} — returning null", meetingLink); + return null; + } + + String slug = meetingLink.substring(idx + 2); + if (slug.isEmpty()) { + logger.warn("[buildRecordingFileName] slug is empty after 'm=' in meetingLink={} — returning null", meetingLink); + return null; + } + + String roomName = roomPrefix + slug; + String fileName = roomName + "/" + roomName + ".mp4"; + return fileName; } -private void saveRecordingFile(String meetingLink) { - try { - // Configurable Jibri recording location - String jibriOutputDir = ConfigProperties.getPropertyByName("jibri.output.path"); // e.g., /srv/jibri/recordings - String saveDir = ConfigProperties.getPropertyByName("video.recording.path"); // e.g., /srv/recordings - - File jibriDir = new File(jibriOutputDir); - File[] matchingFiles = jibriDir.listFiles((dir, name) -> name.contains(meetingLink) && name.endsWith(".mp4")); - - if (matchingFiles != null && matchingFiles.length > 0) { - File recording = matchingFiles[0]; - Path targetPath = Paths.get(saveDir, meetingLink + ".mp4"); - - Files.copy(recording.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING); - logger.info("Recording file saved: " + targetPath); - } else { - logger.warn("No matching recording file found for meeting: " + meetingLink); - } - } catch (IOException e) { - logger.error("Error saving recording file: ", e); + +@Override +public String resolveMeetingLink(String slug) throws Exception { + if (slug == null || slug.isEmpty()) { + throw new IllegalArgumentException("Meeting slug is required"); } + + // SMS clients sometimes include trailing punctuation when linkifying URLs + slug = slug.replaceAll("[.,:;!?]+$", ""); + + String shortLink = jitsiLink + "m=" + slug; + VideoCallParameters params = videoCallRepository.findByMeetingLink(shortLink); + + if (params == null) { + throw new Exception("No meeting found for slug: " + slug); + } + + if (params.isLinkUsed()) { + throw new Exception("This meeting link has already been used and is no longer active."); + } + + String roomName = roomPrefix + slug; + String userName = params.getAgentName() != null && !params.getAgentName().isEmpty() + ? params.getAgentName() + : "Guest"; + + String token = jitsiJwtUtil.generateRoomToken(roomName, userName, defaultUserEmail, false); + String redirectUrl = "https://" + jitsiDomain + "/" + roomName + "?jwt=" + token; + + return redirectUrl; +} + +@Override +public String generateAgentToken(String slug, String agentName, String agentEmail) throws Exception { + if (slug == null || slug.isEmpty()) { + throw new IllegalArgumentException("Meeting slug is required"); + } + + // Room name is deterministic from the slug — no DB lookup needed. + // This avoids a race condition where the frontend calls this endpoint + // before /send-link has written the row. + String roomName = roomPrefix + slug; + String displayName = (agentName != null && !agentName.isEmpty()) ? agentName : "Agent"; + String email = (agentEmail != null && !agentEmail.isEmpty()) ? agentEmail : defaultUserEmail; + + String token = jitsiJwtUtil.generateRoomToken(roomName, displayName, email, true); + return "https://" + jitsiDomain + "/" + roomName + "?jwt=" + token; } } diff --git a/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java b/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java index 1cbb1d8c..397d989f 100644 --- a/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java +++ b/src/main/java/com/iemr/common/service/welcomeSms/WelcomeBenificarySmsServiceImpl.java @@ -77,6 +77,7 @@ public String sendWelcomeSMStoBenificiary(String contactNo, String beneficiaryNa String auth = smsUserName + ":" + smsPassword; headers.add("Authorization", "Basic " + Base64.getEncoder().encodeToString(auth.getBytes())); + headers.setContentType(MediaType.APPLICATION_JSON); logger.info("payload: " + payload); @@ -93,8 +94,8 @@ public String sendWelcomeSMStoBenificiary(String contactNo, String beneficiaryNa } } - - } catch (Exception e) { + } + catch (Exception e) { return "Error sending SMS: " + e.getMessage().toString(); } return null; diff --git a/src/main/java/com/iemr/common/utils/CookieUtil.java b/src/main/java/com/iemr/common/utils/CookieUtil.java index 92c071c5..1562e3c4 100644 --- a/src/main/java/com/iemr/common/utils/CookieUtil.java +++ b/src/main/java/com/iemr/common/utils/CookieUtil.java @@ -35,8 +35,8 @@ public void addJwtTokenToCookie(String Jwttoken, HttpServletResponse response, H // Make the cookie HttpOnly to prevent JavaScript access for security cookie.setHttpOnly(true); - // Set the Max-Age (expiry time) in seconds (8 hours) - cookie.setMaxAge(60 * 60 * 8); // 8 hours expiration + // Set the Max-Age (expiry time) in seconds (10 hours) + cookie.setMaxAge(60 * 60 * 10); // 10 hours expiration // Set the path to "/" so the cookie is available across the entire application cookie.setPath("/"); diff --git a/src/main/java/com/iemr/common/utils/IEMRApplBeans.java b/src/main/java/com/iemr/common/utils/IEMRApplBeans.java index 92d3c339..7747f6ee 100644 --- a/src/main/java/com/iemr/common/utils/IEMRApplBeans.java +++ b/src/main/java/com/iemr/common/utils/IEMRApplBeans.java @@ -40,12 +40,12 @@ @Configuration public class IEMRApplBeans { - @Bean - public KMService getOpenKMService() - { - KMService kmService = new OpenKMServiceImpl(); - return kmService; - } + // @Bean + // public KMService getOpenKMService() + // { + // KMService kmService = new OpenKMServiceImpl(); + // return kmService; + // } @Bean public Validator getVaidator() diff --git a/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java b/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java new file mode 100644 index 00000000..229a77f1 --- /dev/null +++ b/src/main/java/com/iemr/common/utils/JitsiJwtUtil.java @@ -0,0 +1,111 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.utils; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.SecretKey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + +/** + * Mints HS256 JWTs that are accepted by the Jitsi/prosody token-auth module + * running on the video-conferencing host. This is intentionally separate from + * {@link JwtUtil} (which mints application session tokens) because the secret, + * claim set, and expiration policy are completely different. + * + * Claims produced (matches what devops configured on prosody): + * aud -> jitsi.app.id (e.g. "piramal_vc") + * iss -> jitsi.app.id (e.g. "piramal_vc") + * sub -> jitsi.sub (must always be "meet.jitsi") + * room -> the room name to admit the bearer into + * exp -> now + jitsi.token.ttl.seconds + * context.user.{name,email} -> displayed in the Jitsi UI + */ +@Component +public class JitsiJwtUtil { + + // Fallback chains let either dot-form (jitsi.app.id=...) or upper-form + // (JITSI_APP_ID=...) work in any property source, including .properties + // files which Spring does NOT relaxed-bind for @Value. + @Value("${jitsi.app.id:${JITSI_APP_ID:}}") + private String appId; + + @Value("${jitsi.app.secret:${JITSI_APP_SECRET:}}") + private String appSecret; + + @Value("${jitsi.sub:${JITSI_SUB:meet.jitsi}}") + private String sub; + + @Value("${jitsi.token.ttl.seconds:${JITSI_TOKEN_TTL_SECONDS:3600}}") + private long ttlSeconds; + + private SecretKey getSigningKey() { + if (appSecret == null || appSecret.isEmpty()) { + throw new IllegalStateException("jitsi.app.secret is not configured"); + } + return Keys.hmacShaKeyFor(appSecret.getBytes()); + } + + /** + * Build a Jitsi room JWT. + * + * @param room the exact room name the bearer will join (must match the URL path) + * @param userName display name shown in the Jitsi UI + * @param userEmail email shown in the Jitsi UI (used for gravatar etc.) + * @param isModerator when true, grants prosody moderator role — required for "End Meeting for All" + * @return signed compact JWT string + */ + public String generateRoomToken(String room, String userName, String userEmail, boolean isModerator) { + if (room == null || room.isEmpty()) { + throw new IllegalArgumentException("room is required to mint a Jitsi token"); + } + + long nowMs = System.currentTimeMillis(); + Date expiry = new Date(nowMs + (ttlSeconds * 1000L)); + + Map user = new HashMap<>(); + user.put("name", userName != null ? userName : "Guest"); + user.put("email", userEmail != null ? userEmail : ""); + user.put("moderator", isModerator); + + Map context = new HashMap<>(); + context.put("user", user); + + return Jwts.builder() + .header().add("typ", "JWT").and() + .claim("aud", appId) + .issuer(appId) + .subject(sub) + .claim("room", room) + .claim("context", context) + .expiration(expiry) + .signWith(getSigningKey(), Jwts.SIG.HS256) + .compact(); + } +} diff --git a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java index 381f64de..df2d1ed6 100644 --- a/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java +++ b/src/main/java/com/iemr/common/utils/JwtAuthenticationUtil.java @@ -13,7 +13,6 @@ import com.iemr.common.data.users.User; import com.iemr.common.repository.users.IEMRUserRepositoryCustom; -import com.iemr.common.service.users.IEMRAdminUserServiceImpl; import com.iemr.common.utils.exception.IEMRException; import io.jsonwebtoken.Claims; @@ -33,9 +32,6 @@ public class JwtAuthenticationUtil { private IEMRUserRepositoryCustom iEMRUserRepositoryCustom; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - @Autowired - private IEMRAdminUserServiceImpl iEMRAdminUserServiceImpl; - public JwtAuthenticationUtil(CookieUtil cookieUtil, JwtUtil jwtUtil) { this.cookieUtil = cookieUtil; this.jwtUtil = jwtUtil; diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 364aa12d..557d5da5 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -120,6 +120,15 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.info("JwtUserIdValidationFilter invoked for path: " + path); + // Public video-consultation resolve endpoint: hit by SMS recipients on + // phone browsers that have no app session. Skip ALL auth — the JWT minted + // inside the handler + the unguessable slug provide access control. + if (isVideoConsultationResolvePath(path, contextPath)) { + logger.info("Video-consultation resolve path detected - skipping authentication: {}", path); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + // NEW: if this is a platform-feedback endpoint, treat it as public (skip auth) // and also ensure we don't clear any user cookies for these requests. if (isPlatformFeedbackPath(path, contextPath)) { @@ -206,6 +215,17 @@ private boolean isPlatformFeedbackPath(String path, String contextPath) { return normalized.startsWith(base + "/platform-feedback"); } + /** + * Identifies the public video-consultation resolve endpoint. + * Uses multiple matching strategies to be resilient against + * context-path mismatches between reverse-proxy and Wildfly. + */ + private boolean isVideoConsultationResolvePath(String path, String contextPath) { + if (path == null) return false; + String normalized = path.toLowerCase(); + return normalized.endsWith("/video-consultation/resolve") + || normalized.contains("/video-consultation/resolve"); + } private boolean isOriginAllowed(String origin) { if (origin == null || allowedOrigins == null || allowedOrigins.trim().isEmpty()) { @@ -253,7 +273,11 @@ private boolean shouldSkipAuthentication(String path, String contextPath) { || path.startsWith(contextPath + "/user/logOutUserFromConcurrentSession") || path.startsWith(contextPath + "/user/refreshToken") || path.equals(contextPath + "/health") - || path.equals(contextPath + "/version"); + || path.equals(contextPath + "/version") + // Public Jitsi short-link redirect: hit by SMS recipients on phone + // browsers that have no app session. Access control is the JWT minted + // inside the redirect handler + the unguessable slug. + || path.endsWith("/video-consultation/resolve"); } private String getJwtTokenFromCookies(HttpServletRequest request) { diff --git a/src/main/java/com/iemr/common/utils/NetworkUtil.java b/src/main/java/com/iemr/common/utils/NetworkUtil.java new file mode 100644 index 00000000..b5ded4d5 --- /dev/null +++ b/src/main/java/com/iemr/common/utils/NetworkUtil.java @@ -0,0 +1,56 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.utils; + +import java.net.DatagramSocket; +import java.net.InetAddress; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Resolves the single LAN-facing IPv4 address the OS would use for outbound + * traffic, used to build a URL that a device on the same wifi network (e.g. + * a mobile phone) can connect to. A machine can have several network + * interfaces (wifi, ethernet, VPN, virtual adapters); connecting a UDP + * socket to an external address - without sending any packet - makes the OS + * routing table pick the one real outbound interface, avoiding ambiguity + * from iterating all interfaces. + */ +public final class NetworkUtil { + + private static final Logger logger = LoggerFactory.getLogger(NetworkUtil.class); + private static final String FALLBACK_IP = "127.0.0.1"; + + private NetworkUtil() { + } + + public static String getLanIPAddress() { + try (DatagramSocket socket = new DatagramSocket()) { + socket.connect(InetAddress.getByName("8.8.8.8"), 10002); + return socket.getLocalAddress().getHostAddress(); + } catch (Exception e) { + logger.error("Failed to resolve LAN IP address", e); + return FALLBACK_IP; + } + } +} diff --git a/src/main/java/com/iemr/common/utils/config/ConfigProperties.java b/src/main/java/com/iemr/common/utils/config/ConfigProperties.java index 59b69b82..43a49364 100644 --- a/src/main/java/com/iemr/common/utils/config/ConfigProperties.java +++ b/src/main/java/com/iemr/common/utils/config/ConfigProperties.java @@ -144,11 +144,21 @@ public static String getPropertyByName(String propertyName) String result = null; try { - if (properties == null) + if (environment != null) { - initalizeProperties(); + result = environment.getProperty(propertyName); + } + if (result == null) + { + if (properties == null) + { + initalizeProperties(); + } + result = properties.getProperty(propertyName).trim(); + } else + { + result = result.trim(); } - result = properties.getProperty(propertyName).trim(); } catch (Exception e) { logger.error(propertyName + " retrival failed.", e); diff --git a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java index b4aaad60..757e59d9 100644 --- a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java +++ b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java @@ -125,6 +125,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons case "validateSecurityQuestionAndAnswer": case "logOutUserFromConcurrentSession": case "refreshToken": + case "resolve": break; case "error": status = false; diff --git a/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java b/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java index 2be04cfc..1c8c24b2 100644 --- a/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java +++ b/src/main/java/com/iemr/common/utils/km/openkm/OpenKMServiceImpl.java @@ -46,44 +46,47 @@ import com.openkm.sdk4j.exception.VirusDetectedException; import com.openkm.sdk4j.exception.WebserviceException; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.client.JerseyClientBuilder; +import jakarta.annotation.PostConstruct; + + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +@Service +// @Primary public class OpenKMServiceImpl implements KMService { - // private ConfigProperties configProperties; - // - // @Autowired - // public void setConfigProperties(ConfigProperties configProperties) - // { - // this.configProperties = configProperties; - // } + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - private static String url; - private static String username; - private static String password; - private static String kmRootPath; - private static String guestUser; - private static String guestPassword; + @Value("${km-base-url}") + private String url; + + @Value("${km-username}") + private String username; + + @Value("${km-password}") + private String password; + + @Value("${km-root-path}") + private String kmRootPath; + + @Value("${km-guest-user}") + private String guestUser; + + @Value("${km-guest-password}") + private String guestPassword; public OpenKMServiceImpl() { } - private static OKMWebservices connector = null; + private OKMWebservices connector; + @PostConstruct public void init() { - if (connector == null) { - url = ConfigProperties.getPropertyByName("km-base-url"); - username = ConfigProperties.getPropertyByName("km-username"); - password = ConfigProperties.getPropertyByName("km-password"); - kmRootPath = ConfigProperties.getPropertyByName("km-root-path"); - guestUser = ConfigProperties.getPropertyByName("km-guest-user"); - guestPassword = ConfigProperties.getPropertyByName("km-guest-password"); - connector = OpenKMConnector.initialize(url, username, password); + logger.info("KM URL=",url); + connector = OpenKMConnector.initialize(url, username, password); - } - } + } @Override public String getDocumentRoot() { diff --git a/src/main/resources/application-swagger.properties b/src/main/resources/application-swagger.properties index f73e6fd2..74ae2cbb 100644 --- a/src/main/resources/application-swagger.properties +++ b/src/main/resources/application-swagger.properties @@ -23,6 +23,7 @@ secondary.datasource.username= secondary.datasource.password= secondary.datasource.url=jdbc:h2:mem:reportingdb secondary.datasource.driver-class-name=org.h2.Driver +tempFilePath=temp springdoc.api-docs.enabled=true springdoc.swagger-ui.enabled=true diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4ce65cad..4a2de342 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -77,14 +77,14 @@ get-disposition-count-URL=http://CTI_SERVER/apps/CZUtilAPI.php #============================================================================ # Configure Main Scheduler Properties #============================================================================ - + org.quartz.scheduler.instanceId = AUTO org.quartz.scheduler.makeSchedulerThreadDaemon = true - + #============================================================================ # Configure ThreadPool #============================================================================ - + org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.makeThreadsDaemons = true org.quartz.threadPool.threadCount: 20 @@ -172,6 +172,9 @@ quality-Audit-PageSize=5 ## max no of failed login attempt failedLoginAttempt=5 +## account lock duration in hours (24 hours = 1 day for auto-unlock) +account.lock.duration.hours=24 + #Jwt Token configuration jwt.access.expiration=28800000 jwt.refresh.expiration=604800000 @@ -182,14 +185,14 @@ jwt.refresh.expiration=604800000 ## KM Configuration -km-base-protocol=http -km-username=okmAdmin -km-password=admin -km-base-url=http://localhost:8084/OpenKM -km-base-path=localhost:8084/OpenKM -km-root-path=/okm:personal/users/ -km-guest-user=guest -km-guest-password=guest +# km-base-protocol=http +# km-username=okmAdmin +# km-password=admin +# km-base-url=http://localhost:8084/OpenKM +# km-base-path=localhost:8084/OpenKM +# km-root-path=/okm:personal/users/ +# km-guest-user=guest +# km-guest-password=guest # CTI Config cti-server-ip=10.208.122.99 diff --git a/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java b/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java index b3b82380..705beffa 100644 --- a/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java +++ b/src/test/java/com/iemr/common/controller/videocall/VideoCallControllerTest.java @@ -36,7 +36,9 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -195,4 +197,39 @@ void shouldReturnOkWithErrorInBody_whenUpdateCallStatusServiceFails() throws Exc verify(videoCallService, times(1)).updateCallStatus(any(UpdateCallRequest.class)); } + + // Tests for resolveMeetingLink() - public redirect endpoint hit by SMS recipients + @Test + void shouldReturn302WithJitsiUrl_whenResolveMeetingLinkSucceeds() throws Exception { + String fullJitsiUrl = "https://vc.piramalswasthya.org/piramal-meeting-Ab3xQ9pK?jwt=FAKE.JWT.TOKEN"; + when(videoCallService.resolveMeetingLink(eq("Ab3xQ9pK"))).thenReturn(fullJitsiUrl); + + mockMvc.perform(get("/video-consultation/resolve").param("m", "Ab3xQ9pK")) + .andExpect(status().isFound()) + .andExpect(header().string("Location", fullJitsiUrl)); + + verify(videoCallService, times(1)).resolveMeetingLink("Ab3xQ9pK"); + } + + @Test + void shouldReturn400_whenResolveMeetingLinkSlugIsInvalid() throws Exception { + when(videoCallService.resolveMeetingLink(eq(""))) + .thenThrow(new IllegalArgumentException("Meeting slug is required")); + + mockMvc.perform(get("/video-consultation/resolve").param("m", "")) + .andExpect(status().isBadRequest()); + + verify(videoCallService, times(1)).resolveMeetingLink(""); + } + + @Test + void shouldReturn404_whenResolveMeetingLinkSlugUnknown() throws Exception { + when(videoCallService.resolveMeetingLink(eq("missing"))) + .thenThrow(new Exception("No meeting found for slug: missing")); + + mockMvc.perform(get("/video-consultation/resolve").param("m", "missing")) + .andExpect(status().isNotFound()); + + verify(videoCallService, times(1)).resolveMeetingLink("missing"); + } } \ No newline at end of file diff --git a/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java b/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java index baed9029..f8ef8add 100644 --- a/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java +++ b/src/test/java/com/iemr/common/service/videocall/VideoCallServiceImplTest.java @@ -26,6 +26,7 @@ import com.iemr.common.model.videocall.UpdateCallRequest; import com.iemr.common.model.videocall.VideoCallRequest; import com.iemr.common.repository.videocall.VideoCallParameterRepository; +import com.iemr.common.utils.JitsiJwtUtil; import com.iemr.common.utils.config.ConfigProperties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -59,10 +60,15 @@ public class VideoCallServiceImplTest { UpdateCallRequest updateCallRequest; @Mock VideoCallParameters videoCallParameters; + @Mock + JitsiJwtUtil jitsiJwtUtil; @BeforeEach public void setup() throws Exception { ReflectionTestUtils.setField(service, "jitsiLink", "https://meet.jit.si/"); + ReflectionTestUtils.setField(service, "jitsiDomain", "meet.jit.si"); + ReflectionTestUtils.setField(service, "roomPrefix", "piramal-meeting-"); + ReflectionTestUtils.setField(service, "defaultUserEmail", "admin@piramalswasthya.org"); } @Test @@ -175,6 +181,71 @@ public void testSaveRecordingFile_noMatchingFile() throws Exception { } } + @Test + public void testResolveMeetingLink_success() throws Exception { + when(videoCallRepository.findByMeetingLink("https://meet.jit.si/m=Ab3xQ9pK")) + .thenReturn(videoCallParameters); + when(videoCallParameters.getAgentName()).thenReturn("Dr. Asha"); + when(jitsiJwtUtil.generateRoomToken( + eq("piramal-meeting-Ab3xQ9pK"), + eq("Dr. Asha"), + eq("admin@piramalswasthya.org"), + eq(false))).thenReturn("FAKE.JWT.TOKEN"); + + String result = service.resolveMeetingLink("Ab3xQ9pK"); + + assertEquals( + "https://meet.jit.si/piramal-meeting-Ab3xQ9pK?jwt=FAKE.JWT.TOKEN", + result); + verify(jitsiJwtUtil).generateRoomToken( + "piramal-meeting-Ab3xQ9pK", "Dr. Asha", "admin@piramalswasthya.org", false); + } + + @Test + public void testResolveMeetingLink_emptySlug() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> service.resolveMeetingLink("")); + assertEquals("Meeting slug is required", ex.getMessage()); + } + + @Test + public void testResolveMeetingLink_nullSlug() { + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> service.resolveMeetingLink(null)); + assertEquals("Meeting slug is required", ex.getMessage()); + } + + @Test + public void testResolveMeetingLink_notFound() { + when(videoCallRepository.findByMeetingLink("https://meet.jit.si/m=missing")) + .thenReturn(null); + + Exception ex = assertThrows( + Exception.class, + () -> service.resolveMeetingLink("missing")); + assertTrue(ex.getMessage().contains("No meeting found")); + } + + @Test + public void testResolveMeetingLink_fallbackUserNameWhenAgentMissing() throws Exception { + when(videoCallRepository.findByMeetingLink("https://meet.jit.si/m=Ab3xQ9pK")) + .thenReturn(videoCallParameters); + when(videoCallParameters.getAgentName()).thenReturn(null); + when(jitsiJwtUtil.generateRoomToken( + eq("piramal-meeting-Ab3xQ9pK"), + eq("Guest"), + eq("admin@piramalswasthya.org"), + eq(false))).thenReturn("FAKE.JWT.TOKEN"); + + String result = service.resolveMeetingLink("Ab3xQ9pK"); + + assertTrue(result.endsWith("?jwt=FAKE.JWT.TOKEN")); + verify(jitsiJwtUtil).generateRoomToken( + "piramal-meeting-Ab3xQ9pK", "Guest", "admin@piramalswasthya.org", false); + } + @Test public void testSaveRecordingFile_ioException() throws Exception { try (MockedStatic configMock = mockStatic(ConfigProperties.class); diff --git a/src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java b/src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java new file mode 100644 index 00000000..3d89480b --- /dev/null +++ b/src/test/java/com/iemr/common/utils/JitsiJwtUtilTest.java @@ -0,0 +1,144 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Date; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + +class JitsiJwtUtilTest { + + // Same secret format as the one devops gave us (HS256, length must be >=32 bytes for Keys.hmacShaKeyFor) + private static final String APP_ID = "piramal_vc"; + private static final String APP_SECRET = "5b9883418be6f228ffe3ceaa74dd3d3b91737733a4a85c5e82fc584ad449850b"; + private static final String SUB = "meet.jitsi"; + + private JitsiJwtUtil util; + + @BeforeEach + void setUp() { + util = new JitsiJwtUtil(); + ReflectionTestUtils.setField(util, "appId", APP_ID); + ReflectionTestUtils.setField(util, "appSecret", APP_SECRET); + ReflectionTestUtils.setField(util, "sub", SUB); + ReflectionTestUtils.setField(util, "ttlSeconds", 3600L); + } + + @Test + void generateRoomToken_producesAllRequiredClaims() { + String token = util.generateRoomToken("piramal-meeting-Ab3xQ9pK", "Dr. Asha", "asha@piramalswasthya.org", false); + + assertNotNull(token); + assertTrue(token.split("\\.").length == 3, "JWT should have 3 dot-separated parts"); + + Claims claims = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes())) + .build() + .parseSignedClaims(token) + .getPayload(); + + assertEquals(APP_ID, claims.getIssuer()); + assertTrue(claims.getAudience().contains(APP_ID)); + assertEquals(SUB, claims.getSubject()); + assertEquals("piramal-meeting-Ab3xQ9pK", claims.get("room", String.class)); + + @SuppressWarnings("unchecked") + Map context = claims.get("context", Map.class); + assertNotNull(context); + @SuppressWarnings("unchecked") + Map user = (Map) context.get("user"); + assertNotNull(user); + assertEquals("Dr. Asha", user.get("name")); + assertEquals("asha@piramalswasthya.org", user.get("email")); + assertEquals(false, user.get("moderator")); + + Date exp = claims.getExpiration(); + assertNotNull(exp); + assertTrue(exp.after(new Date()), "exp should be in the future"); + } + + @Test + void generateRoomToken_moderatorClaimTrueForAgent() { + String token = util.generateRoomToken("piramal-meeting-Ab3xQ9pK", "Dr. Asha", "asha@piramalswasthya.org", true); + + Claims claims = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes())) + .build() + .parseSignedClaims(token) + .getPayload(); + + @SuppressWarnings("unchecked") + Map context = claims.get("context", Map.class); + @SuppressWarnings("unchecked") + Map user = (Map) context.get("user"); + assertEquals(true, user.get("moderator")); + } + + @Test + void generateRoomToken_fallsBackToGuestWhenUserNameNull() { + String token = util.generateRoomToken("piramal-meeting-xyz", null, null, false); + + Claims claims = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes())) + .build() + .parseSignedClaims(token) + .getPayload(); + + @SuppressWarnings("unchecked") + Map context = claims.get("context", Map.class); + @SuppressWarnings("unchecked") + Map user = (Map) context.get("user"); + assertEquals("Guest", user.get("name")); + assertEquals("", user.get("email")); + } + + @Test + void generateRoomToken_rejectsEmptyRoom() { + assertThrows(IllegalArgumentException.class, + () -> util.generateRoomToken("", "Dr. Asha", "asha@piramalswasthya.org", false)); + } + + @Test + void generateRoomToken_rejectsNullRoom() { + assertThrows(IllegalArgumentException.class, + () -> util.generateRoomToken(null, "Dr. Asha", "asha@piramalswasthya.org", false)); + } + + @Test + void generateRoomToken_failsWhenAppSecretMissing() { + ReflectionTestUtils.setField(util, "appSecret", ""); + assertThrows(IllegalStateException.class, + () -> util.generateRoomToken("piramal-meeting-xyz", "Dr. Asha", "asha@piramalswasthya.org", false)); + } +}