diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7b9a2d8..9d02ab8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Report bug(s) encountered with the gallery api! title: "[Bug]" labels: bug assignees: tjtanjin - --- **Bug Description:** @@ -12,6 +11,7 @@ Provide a clear and concise description of the bug. **Steps To Reproduce:** Steps to reproduce the bug behavior: + 1. Run the command '...' 2. Visit the API url '...' 3. See error @@ -23,13 +23,15 @@ Describe the behavior that is expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser: [e.g. chrome, safari] + +- OS: [e.g. iOS] +- Browser: [e.g. chrome, safari] **Mobile (please complete the following information):** - - Device: [e.g. iPhone12] - - OS: [e.g. iOS8.1] - - Browser: [e.g. edge, firefox] + +- Device: [e.g. iPhone12] +- OS: [e.g. iOS8.1] +- Browser: [e.g. edge, firefox] **Additional Context:** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 21a9d73..f1e10ed 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea to improve the gallery api! title: "[Feat]" labels: enhancement assignees: tjtanjin - --- **Is Your Feature Request Related to a Problem? Please describe:** diff --git a/.github/ISSUE_TEMPLATE/help.md b/.github/ISSUE_TEMPLATE/help.md index 0880541..e657bab 100644 --- a/.github/ISSUE_TEMPLATE/help.md +++ b/.github/ISSUE_TEMPLATE/help.md @@ -4,7 +4,6 @@ about: Seek help with using the gallery api! title: "[Help]" labels: help wanted assignees: tjtanjin - --- **Help Description:** diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md index 3b3390a..98f9f26 100644 --- a/.github/ISSUE_TEMPLATE/task.md +++ b/.github/ISSUE_TEMPLATE/task.md @@ -2,7 +2,6 @@ name: Task about: Create a task to track work to be done for the gallery api (internal use)! title: "[Task]" - --- **Task Description:** @@ -15,6 +14,7 @@ State the expected deliverable(s) for this task (e.g. lint.yml file, configured Add any other context about the task here. **Reminders:** + - Assign task to a project (required) - Assign task to a sprint (required) - Assign task to a developer (optional) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index efd4c49..d28d99b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -23,4 +23,4 @@ Please give a short overview/explanation on the approach taken to resolve the is - [ ] The commit message follows our adopted [guidelines](https://www.conventionalcommits.org/en/v1.0.0/) - [ ] Testing has been done for the change(s) added (for bug fixes/features) -- [ ] Relevant comments/docs have been added/updated (for bug fixes/features) \ No newline at end of file +- [ ] Relevant comments/docs have been added/updated (for bug fixes/features) diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml new file mode 100644 index 0000000..20fc16e --- /dev/null +++ b/.github/workflows/format.yaml @@ -0,0 +1,12 @@ +name: Prettier Workflow +run-name: ${{ github.actor }} running the job +on: [push] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: npm install + - name: Run prettier check + run: npm run format:check diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..d7f3eef --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,12 @@ +name: Lint Workflow +run-name: ${{ github.actor }} running the job +on: [push] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: npm install + - name: Run ESLint + run: npm run lint diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..222861c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/LICENSE.md b/LICENSE.md index c8dddbe..162b937 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 4e9a925..80c2969 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,16 @@
## Table of Contents -* [Introduction](#introduction) -* [Features](#features) -* [Technologies](#technologies) -* [Quickstart](#quickstart) -* [Documentation](#documentation) -* [Team](#team) -* [Contributing](#contributing) -* [Support](#support) -* [Attributions](#attributions) + +- [Introduction](#introduction) +- [Features](#features) +- [Technologies](#technologies) +- [Quickstart](#quickstart) +- [Documentation](#documentation) +- [Team](#team) +- [Contributing](#contributing) +- [Support](#support) +- [Attributions](#attributions) ### Introduction @@ -27,7 +28,9 @@ Apart from a handful of background jobs, the Gallery API largely supports reques For more information on what this project delivers, you may wish to check out the implementation section of the [**Developer Guide**](https://github.com/tjtanjin/react-chatbotify-gallery-api/blob/main/docs/DeveloperGuide.md). ### Technologies + Technologies used by React ChatBotify Gallery API are as below: + #### Done with:@@ -44,14 +47,17 @@ Typescript
#### Project Repository + - https://github.com/tjtanjin/react-chatbotify-gallery-api ### Team -* [Tan Jin](https://github.com/tjtanjin) + +- [Tan Jin](https://github.com/tjtanjin) // todo: the team will be expanded once members are confirmed ### Contributing + If you are looking to contribute to the project, you may find the [**Developer Guide**](https://github.com/tjtanjin/react-chatbotify-gallery-api/blob/main/docs/DeveloperGuide.md) useful. In general, the forking workflow is encouraged and you may open a pull request with clear descriptions on the changes and what they are intended to do (enhancement, bug fixes etc). Alternatively, you may simply raise bugs or suggestions by opening an [**issue**](https://github.com/tjtanjin/react-chatbotify-gallery-api/issues) or raising it up on [**discord**](https://discord.gg/6R4DK4G5Zh). @@ -59,4 +65,5 @@ In general, the forking workflow is encouraged and you may open a pull request w Note: Templates have been created for pull requests and issues to guide you in the process. ### Support -If there are any questions pertaining to the application itself (usage or implementation wise), you may create an [**issue**](https://github.com/tjtanjin/react-chatbotify-gallery-api/issues), raise it up on [**discord**](https://discord.gg/6R4DK4G5Zh), or drop me an email at: **cjtanjin@gmail.com.** \ No newline at end of file + +If there are any questions pertaining to the application itself (usage or implementation wise), you may create an [**issue**](https://github.com/tjtanjin/react-chatbotify-gallery-api/issues), raise it up on [**discord**](https://discord.gg/6R4DK4G5Zh), or drop me an email at: **cjtanjin@gmail.com.** diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index d500a68..9e9ac21 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -92,7 +92,7 @@ services: volumes: - redis-sessions-data-dev:/data - ./config/redis/redis.dev.conf:/usr/local/etc/redis/redis.conf:ro - command: [ "redis-server", "/usr/local/etc/redis/redis.conf" ] + command: ["redis-server", "/usr/local/etc/redis/redis.conf"] healthcheck: test: "exit 0" @@ -131,13 +131,7 @@ services: networks: - core-network-dev healthcheck: - test: - [ - "CMD", - "curl", - "-f", - "http://localhost:9000/minio/health/live" - ] + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 @@ -162,7 +156,6 @@ volumes: minio-data-dev: redis-sessions-data-dev: - networks: nginx-network-dev: driver: bridge diff --git a/docker/docker-compose.playground.yml b/docker/docker-compose.playground.yml index 4e8230c..80d5afc 100644 --- a/docker/docker-compose.playground.yml +++ b/docker/docker-compose.playground.yml @@ -76,7 +76,7 @@ services: volumes: - redis-sessions-data-playground:/data - ./config/redis/redis.playground.conf:/usr/local/etc/redis/redis.conf:ro - command: [ "redis-server", "/usr/local/etc/redis/redis.conf" ] + command: ["redis-server", "/usr/local/etc/redis/redis.conf"] healthcheck: test: "exit 0" @@ -116,13 +116,7 @@ services: networks: - core-network-playground healthcheck: - test: - [ - "CMD", - "curl", - "-f", - "http://localhost:9000/minio/health/live" - ] + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 @@ -147,7 +141,6 @@ volumes: minio-data-playground: redis-sessions-data-playground: - networks: nginx-network-playground: driver: bridge diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 12f6095..d796cf9 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -75,7 +75,7 @@ services: volumes: - redis-sessions-data-prod:/data - ./config/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro - command: [ "redis-server", "/usr/local/etc/redis/redis.conf" ] + command: ["redis-server", "/usr/local/etc/redis/redis.conf"] healthcheck: test: "exit 0" @@ -115,13 +115,7 @@ services: networks: - core-network-prod healthcheck: - test: - [ - "CMD", - "curl", - "-f", - "http://localhost:9000/minio/health/live" - ] + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 20s retries: 3 @@ -146,7 +140,6 @@ volumes: minio-data-prod: redis-sessions-data-prod: - networks: nginx-network-prod: driver: bridge diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index e27c30a..20ea606 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -5,20 +5,21 @@ ## Table of Contents -* [Introduction](#introduction) -* [Navigating this Developer Guide](#navigating-this-developer-guide) -* [Design](#design) -* [Implementation](#implementation) -* [Project Management](#project-management) -* [Code Documentation](#code-documentation) -* [Testing](#testing) -* [Final Notes](#final-notes) +- [Introduction](#introduction) +- [Navigating this Developer Guide](#navigating-this-developer-guide) +- [Design](#design) +- [Implementation](#implementation) +- [Project Management](#project-management) +- [Code Documentation](#code-documentation) +- [Testing](#testing) +- [Final Notes](#final-notes) ## Introduction -Welcome to the Developer Guide for the React Chatbotify Gallery API project. Before diving into this guide, ensure you have gone through the project [*README*](https://github.com/your-repo/react-chatbotify-gallery-website/blob/main/README.md) for an overview. This guide assumes you have a **basic understanding** of the following tools & technologies (or are **at least able to read up and learn about them**): +Welcome to the Developer Guide for the React Chatbotify Gallery API project. Before diving into this guide, ensure you have gone through the project [_README_](https://github.com/your-repo/react-chatbotify-gallery-website/blob/main/README.md) for an overview. This guide assumes you have a **basic understanding** of the following tools & technologies (or are **at least able to read up and learn about them**): + - [**NodeJS**](https://nodejs.org/en) - [**ExpressJS**](https://expressjs.com/) - [**TypeScript**](https://www.typescriptlang.org/) @@ -36,11 +37,11 @@ In addition, you should also have a brief familiarity with [**React ChatBotify** To facilitate your reading, take note of the following syntaxes used throughout this guide: -| Syntax | Description | -|--------------|-----------------------------------------------------------------------------------------------| -| `Code` | Denotes functions, components, or code-related references (e.g., `App`, `useEffect`) | -| *Italics* | Refers to folder or file names (e.g., *App.js*, *components*) | -| **Bold** | Highlights important keywords or concepts | +| Syntax | Description | +| --------- | ------------------------------------------------------------------------------------ | +| `Code` | Denotes functions, components, or code-related references (e.g., `App`, `useEffect`) | +| _Italics_ | Refers to folder or file names (e.g., _App.js_, _components_) | +| **Bold** | Highlights important keywords or concepts | @@ -49,30 +50,31 @@ To facilitate your reading, take note of the following syntaxes used throughout Setting up the project is relatively simple with [**Docker**](https://www.docker.com/). While it is technically feasible to setup the services of the project individually, it requires significantly more time and effort so you're **strongly discouraged** from doing so. The rest of this guide will assume that you have docker installed and have basic familiarity with [**Docker Compose**](https://docs.docker.com/compose/). To setup the project locally, follow the steps below: -1) Fork the [project repository](https://github.com/tjtanjin/react-chatbotify-gallery-api). -2) Clone the **forked project** into your desired directory with: - ``` - git clone {the-forked-project}.git - ``` -3) Next, `cd` into the project and run the following command: - ``` - npm run dev - ``` -4) The API server will be available on **http://localhost:3102**, and you may quickly verify that it is running by visiting the endpoint for fetching themes: http://localhost:3102/api/v1/themes?pageSize=30&pageNum=1 - -**Note:** For internal developers, you will be provided with a *.env.development* file which contains the variables for for the development environment. Notably, you'll be able to interact with the **GitHub Application** meant for development. The development environment is also setup to only **strictly** accept requests from a frontend served at **localhost:3000**. Thus, if you're keen to setup the frontend project, bear in mind to check the port number before calling the backend. For public contributors, you will have to populate the values in *.env.template* from scratch. If you require assistance with that however, feel free to **reach out**! + +1. Fork the [project repository](https://github.com/tjtanjin/react-chatbotify-gallery-api). +2. Clone the **forked project** into your desired directory with: + ``` + git clone {the-forked-project}.git + ``` +3. Next, `cd` into the project and run the following command: + ``` + npm run dev + ``` +4. The API server will be available on **http://localhost:3102**, and you may quickly verify that it is running by visiting the endpoint for fetching themes: http://localhost:3102/api/v1/themes?pageSize=30&pageNum=1 + +**Note:** For internal developers, you will be provided with a _.env.development_ file which contains the variables for for the development environment. Notably, you'll be able to interact with the **GitHub Application** meant for development. The development environment is also setup to only **strictly** accept requests from a frontend served at **localhost:3000**. Thus, if you're keen to setup the frontend project, bear in mind to check the port number before calling the backend. For public contributors, you will have to populate the values in _.env.template_ from scratch. If you require assistance with that however, feel free to **reach out**! ## Design ### Overview -At the root of the project, there are three key directories to be aware of: *config*, *docker*, and *src*. Other files and folders follow typical conventions for such projects and will not be covered in this guide. +At the root of the project, there are three key directories to be aware of: _config_, _docker_, and _src_. Other files and folders follow typical conventions for such projects and will not be covered in this guide. -The *config* directory, as the name implies, contains configuration files. Within this directory, there are three subfolders: *env*, *redis* and *nginx*. The *env* and *redis* subfolder holds environment-specific variables, while the *nginx* subfolder contains a shared NGINX configuration file. +The _config_ directory, as the name implies, contains configuration files. Within this directory, there are three subfolders: _env_, _redis_ and _nginx_. The _env_ and _redis_ subfolder holds environment-specific variables, while the _nginx_ subfolder contains a shared NGINX configuration file. -The *docker* directory includes all files related to Docker. Specifically, it contains Dockerfiles for the api and jobs services, along with Docker Compose files that orchestrate the entire setup. These files are also environment-specific. +The _docker_ directory includes all files related to Docker. Specifically, it contains Dockerfiles for the api and jobs services, along with Docker Compose files that orchestrate the entire setup. These files are also environment-specific. -Lastly, the *src* directory contains all of the application code. It is divided into two subdirectories: *api* and *jobs*, corresponding to the two custom services in the project (as indicated by the separate Dockerfiles). The internal structure of *api* and *jobs* is straightforward and follows common patterns. It is assumed that developers have the necessary expertise to navigate and understand the project structure independently. Therefore, we will focus on discussing the project architecture. +Lastly, the _src_ directory contains all of the application code. It is divided into two subdirectories: _api_ and _jobs_, corresponding to the two custom services in the project (as indicated by the separate Dockerfiles). The internal structure of _api_ and _jobs_ is straightforward and follows common patterns. It is assumed that developers have the necessary expertise to navigate and understand the project structure independently. Therefore, we will focus on discussing the project architecture. ### Architecture @@ -84,7 +86,7 @@ We will briefly describe each of these services below. #### Nginx -The NGINX service acts as the entry point for our backend services. Configurations for NGINX can be found in the *config/nginx* folder. NGINX functions as a load balancer, distributing incoming requests between two API instances (referred to as **api1** and **api2**). If one API instance fails to respond, NGINX will reroute the request to the other instance. +The NGINX service acts as the entry point for our backend services. Configurations for NGINX can be found in the _config/nginx_ folder. NGINX functions as a load balancer, distributing incoming requests between two API instances (referred to as **api1** and **api2**). If one API instance fails to respond, NGINX will reroute the request to the other instance. #### API @@ -92,13 +94,15 @@ The API service handles user requests and interacts with other services to perfo #### Redis -The Redis service is responsible for caching user sessions, user data, and encrypted access tokens. Configuration files for Redis can be found in the *config/redis* folder. There are two Redis instances in the project: +The Redis service is responsible for caching user sessions, user data, and encrypted access tokens. Configuration files for Redis can be found in the _config/redis_ folder. There are two Redis instances in the project: + - **redis-session:** Caches user sessions and is persistent, meaning data is retained across restarts. - **redis-ephemeral:** Caches user data and encrypted access tokens. Data is not retained across restarts, which is acceptable since the refresh token can be used to regenerate this information. #### MySQL The MySQL service functions as the primary database, storing essential user and theme-related data. The following tables are present: + - Users - UserRefreshTokens - Themes @@ -107,7 +111,7 @@ The MySQL service functions as the primary database, storing essential user and - FavoriteThemes - LinkedAuthProviders -The schema for these tables is located in *src/api/databases/sql/models*. Detailed explanations of these tables will be provided later in the guide as they pertain to specific implementations. +The schema for these tables is located in _src/api/databases/sql/models_. Detailed explanations of these tables will be provided later in the guide as they pertain to specific implementations. #### Minio @@ -116,6 +120,7 @@ The MinIO service serves as a temporary storage bucket for theme-related files. #### Jobs There are currently two background jobs: + - **Sync Themes From GitHub:** Runs every 24 hours to ensure the themes data in MySQL is synchronized with the data on GitHub. - **Process Queued Themes:** Runs every 15 minutes to handle the processing of themes queued for creation or deletion. @@ -128,6 +133,7 @@ On top of serving as the backend for the Gallery website, the Gallery API also p ### Nginx Proxies If you've read the **Design** section above, you will be aware that there is an nginx service that proxies requests to 2 API instances. The configurations for nginx are provided below: + ``` events {} @@ -148,7 +154,7 @@ http { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - + # forward the protocol forwarded by the host nginx # note that the gallery platform sits behind 2 proxies proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; @@ -156,6 +162,7 @@ http { } } ``` + Notice that if an API instance returns 500 error codes, nginx will attempt to make a request to the other API instance instead. This is so if an instance unexpectedly crashes, the other instance can continue to serve requests normally as remedy work is done on the other instance. Apart from that, there's another important configuration: `proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;`. This was previously misconfigured as `proxy_set_header X-Forwarded-Proto $scheme;` and took over a day to debug. The reason why `$scheme` **will not work** is because there is actually another nginx server on the host machine as well. The host nginx actually does a proxy pass to the docker nginx with **http** and thus, if you use `$scheme`, it will be **http** instead. @@ -172,12 +179,13 @@ Every 3 months, a session expires and users will be required to login again. Use The application **does not store username or passwords**. Instead, it relies fully on third-party OAuth providers for authenticating users. This allows us to avoid the hassle of having to deal with storage of user passwords, and we can solely focus on delivering the core features of the application. -To flexibly support multiple providers, a set of common fields are defined within *src/api/interfaces/UserProviderData*. Each provider also implements the following set of functions which can be found in their respective files: +To flexibly support multiple providers, a set of common fields are defined within _src/api/interfaces/UserProviderData_. Each provider also implements the following set of functions which can be found in their respective files: + - getUserData, - getUserTokensWithCode, - getUserTokensWithRefresh -Thus, to add additional providers, it is as simple as creating a new file and implementing these functions before introducing them into *src/api/services/authentication*. +Thus, to add additional providers, it is as simple as creating a new file and implementing these functions before introducing them into _src/api/services/authentication_. ### Routes & Endpoints @@ -220,16 +228,19 @@ The progress of the project is tracked using [**GitHub Projects**](https://githu If you are looking to contribute, you are strongly encouraged to take up **good-first-issues** if it is your first time working on the project. If you're not part of the project team but feel confident in taking up issues prefixed with [Task], still feel free to comment and indicate your interest. ### Forking Workflow + This project adopts the [**Forking Workflow**](https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow). In short, here are the steps required: -1) Fork the repository -2) Clone the forked repository to your local device -3) Make your code changes -4) Push to your forked remote repository -5) Open a pull request from your forked repository to the upstream repository (i.e. the main repository) + +1. Fork the repository +2. Clone the forked repository to your local device +3. Make your code changes +4. Push to your forked remote repository +5. Open a pull request from your forked repository to the upstream repository (i.e. the main repository) In addition, developers should fill up the pull requests template diligently. This ensures that changes are well-documented and reviewed before merging. ### Commit Messages + This project adopts [**Conventional Commits**](https://www.conventionalcommits.org/en/v1.0.0/), with a minor difference that **the first word after the commit type is always capitalised**. For example, notice how "A" in "Add" is capitalised in this commit message: `feat: Add initial theme builder layout`. ## Code Documentation @@ -248,6 +259,7 @@ const getUserTokensWithRefresh = async (refreshToken: string) => { // Implementation... } ``` + The above shows an example code comment for a function that fetches new user tokens with the refresh token. Finally, any leftover tasks or areas in the code to be revisited should be flagged with a comment like the one below: diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..1002f70 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,32 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; + +export default [ + { + files: ["**/*.{js,mjs,cjs,ts}"], + languageOptions: { + parser: tsParser, + sourceType: "module", + globals: { + ...globals.browser, + ...globals.node, // Include Node.js globals + }, + }, + plugins: { + "@typescript-eslint": tseslint, + }, + rules: { + ...pluginJs.configs.recommended.rules, + ...tseslint.configs.recommended.rules, + "@typescript-eslint/no-explicit-any": "off", + }, + }, + { + files: ["**/*.js"], + languageOptions: { + sourceType: "script", + }, + }, +]; diff --git a/nodemon-api.json b/nodemon-api.json index 88d98fc..4782a94 100644 --- a/nodemon-api.json +++ b/nodemon-api.json @@ -1,12 +1,6 @@ { - "watch": [ - "src", - "types", - "config" - ], - "ext": "ts,js,json", - "ignore": [ - "node_modules" - ], - "exec": "ts-node ./src/api/index.ts" -} \ No newline at end of file + "watch": ["src", "types", "config"], + "ext": "ts,js,json", + "ignore": ["node_modules"], + "exec": "ts-node ./src/api/index.ts" +} diff --git a/nodemon-jobs.json b/nodemon-jobs.json index 1b92bc3..f5d9a43 100644 --- a/nodemon-jobs.json +++ b/nodemon-jobs.json @@ -1,12 +1,6 @@ { - "watch": [ - "src", - "types", - "config" - ], - "ext": "ts,js,json", - "ignore": [ - "node_modules" - ], - "exec": "ts-node ./src/jobs/index.ts" -} \ No newline at end of file + "watch": ["src", "types", "config"], + "ext": "ts,js,json", + "ignore": ["node_modules"], + "exec": "ts-node ./src/jobs/index.ts" +} diff --git a/package-lock.json b/package-lock.json index 8d94049..2db6780 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,14 +30,19 @@ "uuid": "^9.0.1" }, "devDependencies": { + "@eslint/js": "^9.9.1", "@types/cors": "^2.8.17", "@types/express-session": "^1.18.0", "@types/multer": "^1.4.11", "@types/node": "^22.1.0", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^9.0.8", + "eslint": "^9.9.1", + "globals": "^15.9.0", + "prettier": "3.3.3", "ts-node": "^10.9.2", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "typescript-eslint": "^8.2.0" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -92,6 +97,181 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -122,6 +302,41 @@ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -344,22 +559,309 @@ "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", "dev": true, - "license": "MIT", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, + "node_modules/@types/validator": { + "version": "13.11.9", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", + "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", + "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/type-utils": "8.2.0", + "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", + "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", + "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", + "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/utils": "8.2.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/types": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", + "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", + "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", + "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", + "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", + "dev": true, "dependencies": { - "@types/express": "*", - "@types/serve-static": "*" + "@typescript-eslint/types": "8.2.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true - }, - "node_modules/@types/validator": { - "version": "13.11.9", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", - "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", @@ -396,9 +898,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -407,6 +909,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", @@ -497,6 +1008,15 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -732,11 +1252,57 @@ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" }, + "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, + "engines": { + "node": ">=6" + } + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, + "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, + "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/chalk/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -929,6 +1495,20 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -956,6 +1536,12 @@ "node": ">=0.10" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1014,6 +1600,18 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1092,6 +1690,190 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.1", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -1203,11 +1985,33 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -1229,6 +2033,27 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1266,6 +2091,41 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -1453,6 +2313,38 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -1464,6 +2356,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -1599,11 +2497,45 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflection": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", @@ -1718,7 +2650,16 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/is-property": { @@ -1750,6 +2691,12 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/isomorphic-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", @@ -1780,6 +2727,12 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -1790,6 +2743,12 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -1809,6 +2768,43 @@ "node": ">=0.6.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "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", @@ -1824,6 +2820,12 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, + "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 + }, "node_modules/lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", @@ -1864,6 +2866,15 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1872,6 +2883,19 @@ "node": ">= 0.6" } }, + "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, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -2063,6 +3087,12 @@ "node": ">=12" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -2258,6 +3288,65 @@ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", "peer": true }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "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, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2266,6 +3355,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2274,11 +3372,29 @@ "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, + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -2308,6 +3424,30 @@ "node": ">= 0.4" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2379,6 +3519,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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" + } + ] + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -2513,11 +3673,53 @@ "uuid": "bin/uuid" } }, + "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, + "engines": { + "node": ">=4" + } + }, "node_modules/retry-as-promised": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2727,6 +3929,27 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "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, + "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, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -2755,6 +3978,15 @@ "node": ">=10" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -2869,6 +4101,18 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strnum": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", @@ -2935,6 +4179,12 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -3009,6 +4259,18 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -3068,6 +4330,18 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3098,6 +4372,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.2.0.tgz", + "integrity": "sha512-DmnqaPcML0xYwUzgNbM1XaKXpEb7BShYf2P1tkUmmcl8hyeG7Pj08Er7R9bNy6AufabywzJcOybQAtnD/c9DGw==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.2.0", + "@typescript-eslint/parser": "8.2.0", + "@typescript-eslint/utils": "8.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -3245,6 +4542,21 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", @@ -3271,6 +4583,15 @@ "@types/node": "*" } }, + "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, + "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", @@ -3342,6 +4663,18 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/z-schema": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", diff --git a/package.json b/package.json index 275d219..44ec435 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,19 @@ "main": "index.js", "license": "MIT", "devDependencies": { + "@eslint/js": "^9.9.1", "@types/cors": "^2.8.17", "@types/express-session": "^1.18.0", "@types/multer": "^1.4.11", "@types/node": "^22.1.0", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^9.0.8", + "eslint": "^9.9.1", + "globals": "^15.9.0", + "prettier": "3.3.3", "ts-node": "^10.9.2", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "typescript-eslint": "^8.2.0" }, "scripts": { "build": "npx tsc --build", @@ -21,7 +26,10 @@ "stop-playground": "docker compose -p playground -f ./docker/docker-compose.playground.yml down", "dev": "docker compose -p dev -f ./docker/docker-compose.dev.yml down && docker compose -p dev -f ./docker/docker-compose.dev.yml up --build", "dev:watch-api": "nodemon --config nodemon-api.json", - "dev:watch-jobs": "nodemon --config nodemon-jobs.json" + "dev:watch-jobs": "nodemon --config nodemon-jobs.json", + "format:write": "prettier . --write", + "format:check": "prettier . --check", + "lint": "eslint --fix ." }, "dependencies": { "@types/express": "^4.17.17", @@ -44,4 +52,4 @@ "swagger-ui-express": "^5.0.1", "uuid": "^9.0.1" } -} \ No newline at end of file +} diff --git a/src/api/controllers/authController.ts b/src/api/controllers/authController.ts index de9a433..54dbb9a 100644 --- a/src/api/controllers/authController.ts +++ b/src/api/controllers/authController.ts @@ -1,68 +1,77 @@ import { Request, Response } from "express"; -import { fetchTokensWithCode, getUserData, saveUserTokens } from "../services/authentication/authentication"; +import { + fetchTokensWithCode, + getUserData, + saveUserTokens, +} from "../services/authentication/authentication"; import { encrypt } from "../services/cryptoService"; /** * Handles the callback when a user authorizes or rejects the application. - * + * * @param req request from call * @param res response to call - * + * * @returns redirects user to login process page on frontend on success, error page otherwise */ const handleCallback = async (req: Request, res: Response) => { - // todo: re-direct user to a more specific error page instead of a generic one - need to liase with frontend team - if (req.query.error === "access_denied") { - return res.redirect(`${process.env.FRONTEND_WEBSITE_URL}/error`); - } - try { - const key = encrypt(req.query.code as string); - res.redirect(`${process.env.FRONTEND_WEBSITE_URL}/login/process?provider=${process.env.GITHUB_LOGIN_PROVIDER}&key=${key}`); - } catch { - // todo: re-direct user to a more specific error page instead of a generic one - need to liase with frontend team - return res.redirect(`${process.env.FRONTEND_WEBSITE_URL}/error`); - } + // todo: re-direct user to a more specific error page instead of a generic one - need to liase with frontend team + if (req.query.error === "access_denied") { + return res.redirect(`${process.env.FRONTEND_WEBSITE_URL}/error`); + } + try { + const key = encrypt(req.query.code as string); + res.redirect( + `${process.env.FRONTEND_WEBSITE_URL}/login/process?provider=${process.env.GITHUB_LOGIN_PROVIDER}&key=${key}`, + ); + } catch { + // todo: re-direct user to a more specific error page instead of a generic one - need to liase with frontend team + return res.redirect(`${process.env.FRONTEND_WEBSITE_URL}/error`); + } }; /** * Handles login processing by first using the auth code to retrieve the access and refresh token. * Following which, use the access token to fetch user data. Tokens and user data are both stored * in a redis cache for ease of retrieval - * + * * @param req request from call * @param res response to call - * + * * @returns user data on success, 401 unauthorized otherwise */ const handleLoginProcess = async (req: Request, res: Response) => { - const sessionId = req.sessionID; - const provider = req.query.provider as string; + const sessionId = req.sessionID; + const provider = req.query.provider as string; - // if no provider specified, unable to login - if (!provider) { - return res.status(401).json({ error: "No login provider found, please try again." }); - } + // if no provider specified, unable to login + if (!provider) { + return res + .status(401) + .json({ error: "No login provider found, please try again." }); + } - // if unable to fetch user tokens, get user to login again - const tokenResponse = await fetchTokensWithCode(sessionId, req.query.key as string, provider); - if (!tokenResponse) { - return res.status(401).json({ error: "Login failed, please try again." }); - } + // if unable to fetch user tokens, get user to login again + const tokenResponse = await fetchTokensWithCode( + sessionId, + req.query.key as string, + provider, + ); + if (!tokenResponse) { + return res.status(401).json({ error: "Login failed, please try again." }); + } - // get user data (will create user if new) - const userData = await getUserData(sessionId, null, provider); - if (!userData) { - return res.status(401).json({ error: "Login failed, please try again." }); - } + // get user data (will create user if new) + const userData = await getUserData(sessionId, null, provider); + if (!userData) { + return res.status(401).json({ error: "Login failed, please try again." }); + } - req.session.provider = provider; - req.session.userId = userData.id; - saveUserTokens(sessionId, userData.id, tokenResponse); - res.json(userData); + req.session.provider = provider; + req.session.userId = userData.id; + saveUserTokens(sessionId, userData.id, tokenResponse); + res.json(userData); }; -export { - handleCallback, - handleLoginProcess -}; +export { handleCallback, handleLoginProcess }; diff --git a/src/api/controllers/themeController.ts b/src/api/controllers/themeController.ts index 60b3123..ad95af2 100644 --- a/src/api/controllers/themeController.ts +++ b/src/api/controllers/themeController.ts @@ -14,32 +14,35 @@ import { checkIsAdminUser } from "../services/authorization"; * @returns list of themes on success, 500 error otherwise */ const getThemes = async (req: Request, res: Response) => { - // default to returning 30 themes per page - // default to returning only first page if not specified - // default to no searches - const { pageSize = 30, pageNum = 1, searchQuery = "" } = req.query; - - // construct clause for searching themes - const limit = parseInt(pageSize as string, 30); - const offset = (parseInt(pageNum as string, 30) - 1) * limit; - const whereClause = searchQuery ? { - [Op.or]: [ - { name: { [Op.like]: `%${searchQuery}%` } }, - { description: { [Op.like]: `%${searchQuery}%` } } - ] - } : {}; - - // fetch themes according to page size, page num and search query - try { - const themes = await Theme.findAll({ - where: whereClause, - limit, - offset - }); - res.json(themes); - } catch (error) { - res.status(500).json({ error: "Failed to fetch themes" }); - } + // default to returning 30 themes per page + // default to returning only first page if not specified + // default to no searches + const { pageSize = 30, pageNum = 1, searchQuery = "" } = req.query; + + // construct clause for searching themes + const limit = parseInt(pageSize as string, 30); + const offset = (parseInt(pageNum as string, 30) - 1) * limit; + const whereClause = searchQuery + ? { + [Op.or]: [ + { name: { [Op.like]: `%${searchQuery}%` } }, + { description: { [Op.like]: `%${searchQuery}%` } }, + ], + } + : {}; + + // fetch themes according to page size, page num and search query + try { + const themes = await Theme.findAll({ + where: whereClause, + limit, + offset, + }); + res.json(themes); + } catch (error) { + res.status(500).json({ error: "Failed to fetch themes" }); + console.log(error); + } }; /** @@ -51,59 +54,63 @@ const getThemes = async (req: Request, res: Response) => { * @returns list of theme versions on success, 500 error otherwise */ const getThemeVersions = async (req: Request, res: Response) => { - try { - const versions = await ThemeVersion.findAll({ - where: { theme_id: req.query.themeId } - }); - - res.json(versions); - } catch (error) { - console.error("Error fetching theme versions:", error); - res.status(500).json({ error: "Failed to fetch theme versions" }); - } + try { + const versions = await ThemeVersion.findAll({ + where: { theme_id: req.query.themeId }, + }); + + res.json(versions); + } catch (error) { + console.error("Error fetching theme versions:", error); + res.status(500).json({ error: "Failed to fetch theme versions" }); + } }; /** * Publishes a new theme (including version bumps). - * + * * @param req request from call * @param res response to call * * @returns 201 on success, 500 otherwise */ const publishTheme = async (req: Request, res: Response) => { - const userData = req.userData; - const { theme_id, name, description, version } = req.body; - - // todo: perform checks in the following steps: - // 1) if theme_id already exist and user is not author, 403 - // 2) if theme_id already exist and user is author but version already exist, 400 - // 3) if theme_id does not exist or user is author of theme but has no existing version, continue below - // 4) rigorously validate file inputs (styles.json, styles.css, settings.json) - // 5) if fail checks, immediately return and don't do any further queuing or processing - // 6) provide verbose reasons for frontend to render to user - const validationPassed = true; - if (!validationPassed) { - return res.status(400).json({ error: "Failed to publish theme, validation failed." }); - } - - // add the new creation to theme job queue for processing later - try { - await ThemeJobQueue.create({ - user_id: userData.id, - theme_id, - name, - description, - action: "CREATE" - }); - - // todo: push files into minio bucket with theme_id for process queue job to pick up - - res.status(201); - } catch (error) { - console.error("Error publishing theme:", error); - res.status(500).json({ error: "Failed to publish theme, please try again." }); - } + const userData = req.userData; + const { theme_id, name, description } = req.body; + + // todo: perform checks in the following steps: + // 1) if theme_id already exist and user is not author, 403 + // 2) if theme_id already exist and user is author but version already exist, 400 + // 3) if theme_id does not exist or user is author of theme but has no existing version, continue below + // 4) rigorously validate file inputs (styles.json, styles.css, settings.json) + // 5) if fail checks, immediately return and don't do any further queuing or processing + // 6) provide verbose reasons for frontend to render to user + const validationPassed = true; + if (!validationPassed) { + return res + .status(400) + .json({ error: "Failed to publish theme, validation failed." }); + } + + // add the new creation to theme job queue for processing later + try { + await ThemeJobQueue.create({ + user_id: userData.id, + theme_id, + name, + description, + action: "CREATE", + }); + + // todo: push files into minio bucket with theme_id for process queue job to pick up + + res.status(201); + } catch (error) { + console.error("Error publishing theme:", error); + res + .status(500) + .json({ error: "Failed to publish theme, please try again." }); + } }; /** @@ -115,51 +122,49 @@ const publishTheme = async (req: Request, res: Response) => { * @returns 200 on success, 500 otherwise */ const unpublishTheme = async (req: Request, res: Response) => { - const userData = req.userData; - const { theme_id } = req.params; - - // check if the theme exists and is owned by the user - try { - const theme = await Theme.findOne({ - where: { - id: theme_id, - } - }); - - // if theme does not exist, cannot delete - if (!theme) { - return res.status(404).json({ error: "Failed to unpublish theme, the theme does not exist." }); - } - - // if theme exist and user is admin, can delete - const isAdminUser = checkIsAdminUser(userData); - if (isAdminUser) { - // todo: allow admins to forcibly unpublish themes - } - - // todo: review how to handle unpublishing of themes, authors should not be allowed to delete themes anytime - // as there may be existing projects using their themes - perhaps separately have a support system for such action - return res.status(400).json({ error: "Feature not allowed." }); - - // if theme exist but user is not the theme author, cannot delete - // if (theme.dataValues.user_id != req.session.userId) { - // return res.status(403).json({ error: "Failed to unpublish theme, you are not the theme author." }); - // } - - // delete the theme - // await theme.destroy(); - - // res.status(200); - } catch (error) { - console.error("Error unpublishing theme:", error); - res.status(500).json({ error: "Failed to unpublish theme, please try again." }); - } -}; - -export { - getThemes, - getThemeVersions, - publishTheme, - unpublishTheme + const userData = req.userData; + const { theme_id } = req.params; + + // check if the theme exists and is owned by the user + try { + const theme = await Theme.findOne({ + where: { + id: theme_id, + }, + }); + + // if theme does not exist, cannot delete + if (!theme) { + return res.status(404).json({ + error: "Failed to unpublish theme, the theme does not exist.", + }); + } + + // if theme exist and user is admin, can delete + const isAdminUser = checkIsAdminUser(userData); + if (isAdminUser) { + // todo: allow admins to forcibly unpublish themes + } + + // todo: review how to handle unpublishing of themes, authors should not be allowed to delete themes anytime + // as there may be existing projects using their themes - perhaps separately have a support system for such action + return res.status(400).json({ error: "Feature not allowed." }); + + // if theme exist but user is not the theme author, cannot delete + // if (theme.dataValues.user_id != req.session.userId) { + // return res.status(403).json({ error: "Failed to unpublish theme, you are not the theme author." }); + // } + + // delete the theme + // await theme.destroy(); + + // res.status(200); + } catch (error) { + console.error("Error unpublishing theme:", error); + res + .status(500) + .json({ error: "Failed to unpublish theme, please try again." }); + } }; +export { getThemes, getThemeVersions, publishTheme, unpublishTheme }; diff --git a/src/api/controllers/userController.ts b/src/api/controllers/userController.ts index db4dc9a..71e9f4e 100644 --- a/src/api/controllers/userController.ts +++ b/src/api/controllers/userController.ts @@ -13,17 +13,21 @@ import { checkIsAdminUser } from "../services/authorization"; * @returns user data if successful, 403 otherwise */ const getUserProfile = async (req: Request, res: Response) => { - const userData = req.userData; - const queryUserId = req.query.userId as string; - const sessionUserId = req.session.userId; - - // if user id matches or user is admin, can retrieve user data - if (!queryUserId || queryUserId === sessionUserId || checkIsAdminUser(userData)) { - return res.json(userData); - } - - // all other cases unauthorized - return res.status(403).json({ error: "Unauthorized access" }); + const userData = req.userData; + const queryUserId = req.query.userId as string; + const sessionUserId = req.session.userId; + + // if user id matches or user is admin, can retrieve user data + if ( + !queryUserId || + queryUserId === sessionUserId || + checkIsAdminUser(userData) + ) { + return res.json(userData); + } + + // all other cases unauthorized + return res.status(403).json({ error: "Unauthorized access" }); }; /** @@ -35,25 +39,31 @@ const getUserProfile = async (req: Request, res: Response) => { * @returns list of user's themes if successful, 403 otherwise */ const getUserThemes = async (req: Request, res: Response) => { - const userData = req.userData; - const queryUserId = req.query.userId as string; - const sessionUserId = req.session.userId; - - // if user id matches or user is admin, can retrieve user themes - if (!queryUserId || queryUserId === sessionUserId || checkIsAdminUser(userData)) { - try { - const themes = await Theme.findAll({ - where: { - user_id: userData.id - } - }); - return res.json(themes); - } catch { - } - } - - // all other cases unauthorized - return res.status(403).json({ error: "Unauthorized access" }); + const userData = req.userData; + const queryUserId = req.query.userId as string; + const sessionUserId = req.session.userId; + + // if user id matches or user is admin, can retrieve user themes + if ( + !queryUserId || + queryUserId === sessionUserId || + checkIsAdminUser(userData) + ) { + try { + const themes = await Theme.findAll({ + where: { + user_id: userData.id, + }, + }); + return res.json(themes); + } catch (error) { + res.status(500).json({ error: "Failed to fetch themes" }); + console.log(error); + } + } + + // all other cases unauthorized + return res.status(403).json({ error: "Unauthorized access" }); }; /** @@ -65,26 +75,32 @@ const getUserThemes = async (req: Request, res: Response) => { * @returns list of user's favorited themes if successful, 403 otherwise */ const getUserFavoriteThemes = async (req: Request, res: Response) => { - const userData = req.userData; - const queryUserId = req.query.userId as string; - const sessionUserId = req.session.userId; - - // if user id matches or user is admin, can retrieve user favorited themes - if (!queryUserId || queryUserId === sessionUserId || checkIsAdminUser(userData)) { - try { - const userFavoriteThemes = await FavoriteTheme.findAll({ - where: { - user_id: userData.id - }, - include: [Theme] - }); - res.json(userFavoriteThemes); - } catch { - } - } - - // all other cases unauthorized - return res.status(403).json({ error: "Unauthorized access" }); + const userData = req.userData; + const queryUserId = req.query.userId as string; + const sessionUserId = req.session.userId; + + // if user id matches or user is admin, can retrieve user favorited themes + if ( + !queryUserId || + queryUserId === sessionUserId || + checkIsAdminUser(userData) + ) { + try { + const userFavoriteThemes = await FavoriteTheme.findAll({ + where: { + user_id: userData.id, + }, + include: [Theme], + }); + res.json(userFavoriteThemes); + } catch (error) { + res.status(500).json({ error: "Failed to fetch user favorite themes" }); + console.log(error); + } + } + + // all other cases unauthorized + return res.status(403).json({ error: "Unauthorized access" }); }; /** @@ -96,45 +112,48 @@ const getUserFavoriteThemes = async (req: Request, res: Response) => { * @returns 201 if successful, 404 if theme not found, 400 if already favorited, 500 otherwise */ const addUserFavoriteTheme = async (req: Request, res: Response) => { - const userData = req.userData; - const { theme_id } = req.body; - - try { - await sequelize.transaction(async (transaction) => { - // check if the theme exists - const theme = await Theme.findByPk(theme_id, { transaction }); - if (!theme) { - return res.status(404).json({ error: "Theme not found." }); - } - - // check if theme already favorited - const existingFavorite = await FavoriteTheme.findOne({ - where: { - user_id: userData.id, - id: theme_id - }, - transaction - }); - - if (existingFavorite) { - return res.status(400).json({ error: "Theme already favorited." }); - } - - // add favorite theme - await FavoriteTheme.create({ - user_id: userData.id, - id: theme_id - }, { transaction }); - - // increment the favorites count in the theme table - await theme.increment("favorites_count", { by: 1, transaction }); - }); - - res.status(201); - } catch (error) { - console.error("Error adding favorite theme:", error); - res.status(500).json({ error: "Failed to add favorite theme." }); - } + const userData = req.userData; + const { theme_id } = req.body; + + try { + await sequelize.transaction(async (transaction) => { + // check if the theme exists + const theme = await Theme.findByPk(theme_id, { transaction }); + if (!theme) { + return res.status(404).json({ error: "Theme not found." }); + } + + // check if theme already favorited + const existingFavorite = await FavoriteTheme.findOne({ + where: { + user_id: userData.id, + id: theme_id, + }, + transaction, + }); + + if (existingFavorite) { + return res.status(400).json({ error: "Theme already favorited." }); + } + + // add favorite theme + await FavoriteTheme.create( + { + user_id: userData.id, + id: theme_id, + }, + { transaction }, + ); + + // increment the favorites count in the theme table + await theme.increment("favorites_count", { by: 1, transaction }); + }); + + res.status(201); + } catch (error) { + console.error("Error adding favorite theme:", error); + res.status(500).json({ error: "Failed to add favorite theme." }); + } }; /** @@ -146,42 +165,45 @@ const addUserFavoriteTheme = async (req: Request, res: Response) => { * @returns 200 if successful, 404 if theme not found, 500 otherwise */ const removeUserFavoriteTheme = async (req: Request, res: Response) => { - const userData = req.userData; - const { theme_id } = req.params; - - try { - await sequelize.transaction(async (transaction) => { - // check if theme is favorited - const existingFavorite = await FavoriteTheme.findOne({ - where: { - user_id: userData.id, - id: theme_id - }, - transaction - }); - - if (!existingFavorite) { - return res.status(404).json({ error: "Favorite theme not found" }); - } - - // remove favorite theme - await existingFavorite.destroy({ transaction }); - - // decrement the favorites count in the theme table - const theme = await Theme.findByPk(theme_id, { transaction }); - if (theme) { - await theme.decrement("favorites_count", { by: 1, transaction }); - } - }); - - res.status(200); - } catch (error) { - console.error("Error removing favorite theme:", error); - res.status(500).json({ error: "Failed to remove favorite theme" }); - } + const userData = req.userData; + const { theme_id } = req.params; + + try { + await sequelize.transaction(async (transaction) => { + // check if theme is favorited + const existingFavorite = await FavoriteTheme.findOne({ + where: { + user_id: userData.id, + id: theme_id, + }, + transaction, + }); + + if (!existingFavorite) { + return res.status(404).json({ error: "Favorite theme not found" }); + } + + // remove favorite theme + await existingFavorite.destroy({ transaction }); + + // decrement the favorites count in the theme table + const theme = await Theme.findByPk(theme_id, { transaction }); + if (theme) { + await theme.decrement("favorites_count", { by: 1, transaction }); + } + }); + + res.status(200); + } catch (error) { + console.error("Error removing favorite theme:", error); + res.status(500).json({ error: "Failed to remove favorite theme" }); + } }; export { - addUserFavoriteTheme, getUserFavoriteThemes, getUserProfile, - getUserThemes, removeUserFavoriteTheme + addUserFavoriteTheme, + getUserFavoriteThemes, + getUserProfile, + getUserThemes, + removeUserFavoriteTheme, }; diff --git a/src/api/databases/redis.ts b/src/api/databases/redis.ts index 4624df0..ea14709 100644 --- a/src/api/databases/redis.ts +++ b/src/api/databases/redis.ts @@ -3,31 +3,28 @@ import { createClient } from "redis"; // initialize redis session client const redisSessionClient = createClient({ - socket: { - host: "redis-sessions", - port: 6379, - // todo: protect with passphrase? - } -}) -redisSessionClient.connect().catch(console.error) + socket: { + host: "redis-sessions", + port: 6379, + // todo: protect with passphrase? + }, +}); +redisSessionClient.connect().catch(console.error); const redisSessionStore = new RedisStore({ - client: redisSessionClient, - // matches express cookie expiry duration (redis store specifies ttl in seconds) - ttl: 7776000 + client: redisSessionClient, + // matches express cookie expiry duration (redis store specifies ttl in seconds) + ttl: 7776000, }); // initialize redis ephemeral client const redisEphemeralClient = createClient({ - socket: { - host: "redis-ephemeral", - port: 6379, - // todo: protect with passphrase? - } + socket: { + host: "redis-ephemeral", + port: 6379, + // todo: protect with passphrase? + }, }); redisEphemeralClient.connect().catch(console.error); -export { - redisEphemeralClient, - redisSessionStore -}; +export { redisEphemeralClient, redisSessionStore }; diff --git a/src/api/databases/sql/models/FavoriteTheme.ts b/src/api/databases/sql/models/FavoriteTheme.ts index 520eea7..8726b3b 100644 --- a/src/api/databases/sql/models/FavoriteTheme.ts +++ b/src/api/databases/sql/models/FavoriteTheme.ts @@ -6,7 +6,7 @@ import User from "./User"; /** * Association table between a user and a theme (user favorite theme). */ -class FavoriteTheme extends Model { } +class FavoriteTheme extends Model {} FavoriteTheme.init({}, { sequelize, modelName: "FavoriteTheme" }); diff --git a/src/api/databases/sql/models/LinkedAuthProvider.ts b/src/api/databases/sql/models/LinkedAuthProvider.ts index 2b2ccef..766fe1d 100644 --- a/src/api/databases/sql/models/LinkedAuthProvider.ts +++ b/src/api/databases/sql/models/LinkedAuthProvider.ts @@ -5,37 +5,40 @@ import User from "./User"; /** * Stores the login providers associated with a user. */ -class LinkedAuthProvider extends Model { } +class LinkedAuthProvider extends Model {} -LinkedAuthProvider.init({ - // user id from the login provider (e.g. github user id) - provider_user_id: { - type: DataTypes.STRING, - primaryKey: true - }, - // user id for the user created in this application - user_id: { - type: DataTypes.UUID, - allowNull: false, - references: { - model: User, - key: "id" - } - }, - // name of the login provider - provider: { - type: DataTypes.STRING, - allowNull: false - }, - // date when the link was done - created_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - }, -}, { - sequelize, - modelName: "LinkedAuthProvider", - timestamps: false -}); +LinkedAuthProvider.init( + { + // user id from the login provider (e.g. github user id) + provider_user_id: { + type: DataTypes.STRING, + primaryKey: true, + }, + // user id for the user created in this application + user_id: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: User, + key: "id", + }, + }, + // name of the login provider + provider: { + type: DataTypes.STRING, + allowNull: false, + }, + // date when the link was done + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + }, + { + sequelize, + modelName: "LinkedAuthProvider", + timestamps: false, + }, +); export default LinkedAuthProvider; diff --git a/src/api/databases/sql/models/Theme.ts b/src/api/databases/sql/models/Theme.ts index eaf1d94..98e5795 100644 --- a/src/api/databases/sql/models/Theme.ts +++ b/src/api/databases/sql/models/Theme.ts @@ -5,48 +5,51 @@ import User from "./User"; /** * Stores the data of themes. */ -class Theme extends Model { } +class Theme extends Model {} -Theme.init({ - // unique identifier for the theme, matches github themes folder name (e.g. minimal_midnight) - id: { - type: DataTypes.STRING, - primaryKey: true - }, - // name of the theme, a more human-readable friendly identifier but may not be unique - name: { - type: DataTypes.STRING, - allowNull: false - }, - // a brief description of the theme - description: { - type: DataTypes.TEXT, - allowNull: true - }, - // number of favorites given to the theme - favorites_count: { - type: DataTypes.INTEGER, - defaultValue: 0 - }, - // date when the theme is created, based on when it was synced in from github - created_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - }, - // todo: currently this field isn"t updated because version isn"t integrated yet - // date when the theme was last updated, based on when sync detects a version upgrade - updated_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - } -}, { - sequelize, - modelName: "Theme", - timestamps: false -}); +Theme.init( + { + // unique identifier for the theme, matches github themes folder name (e.g. minimal_midnight) + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + // name of the theme, a more human-readable friendly identifier but may not be unique + name: { + type: DataTypes.STRING, + allowNull: false, + }, + // a brief description of the theme + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + // number of favorites given to the theme + favorites_count: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + // date when the theme is created, based on when it was synced in from github + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + // todo: currently this field isn"t updated because version isn"t integrated yet + // date when the theme was last updated, based on when sync detects a version upgrade + updated_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + }, + { + sequelize, + modelName: "Theme", + timestamps: false, + }, +); // theme belongs to a user, but permitted to be empty (for direct theme contributions to github repository) // todo: perhaps the sync job csn attempt to reconcile theme ownership each time it is run based on meta.json author? Theme.belongsTo(User, { foreignKey: "user_id" }); -export default Theme; \ No newline at end of file +export default Theme; diff --git a/src/api/databases/sql/models/ThemeJobQueue.ts b/src/api/databases/sql/models/ThemeJobQueue.ts index 68d7031..59fb7f5 100644 --- a/src/api/databases/sql/models/ThemeJobQueue.ts +++ b/src/api/databases/sql/models/ThemeJobQueue.ts @@ -4,39 +4,42 @@ import { sequelize } from "../sql"; /** * Serves as a job queue for the themes that need to be processed. */ -class ThemeJobQueue extends Model { } +class ThemeJobQueue extends Model {} -ThemeJobQueue.init({ - // id to uniquely identify the job - id: { - type: DataTypes.STRING, - primaryKey: true - }, - // name of the theme, used to generate meta.json and copied into the theme table - name: { - type: DataTypes.STRING, - allowNull: false - }, - // description of the theme, used to generate meta.json and copied into the theme table - description: { - type: DataTypes.TEXT, - allowNull: true - }, - // action for this job (create or delete theme) - action: { - type: DataTypes.ENUM, - values: ["CREATE", "DELETE"], - allowNull: false - }, - // date of job creation - created_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - } -}, { - sequelize, - modelName: "ThemeJob", - timestamps: false -}); +ThemeJobQueue.init( + { + // id to uniquely identify the job + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + // name of the theme, used to generate meta.json and copied into the theme table + name: { + type: DataTypes.STRING, + allowNull: false, + }, + // description of the theme, used to generate meta.json and copied into the theme table + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + // action for this job (create or delete theme) + action: { + type: DataTypes.ENUM, + values: ["CREATE", "DELETE"], + allowNull: false, + }, + // date of job creation + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + }, + { + sequelize, + modelName: "ThemeJob", + timestamps: false, + }, +); export default ThemeJobQueue; diff --git a/src/api/databases/sql/models/ThemeVersion.ts b/src/api/databases/sql/models/ThemeVersion.ts index 99230a1..f583be1 100644 --- a/src/api/databases/sql/models/ThemeVersion.ts +++ b/src/api/databases/sql/models/ThemeVersion.ts @@ -6,40 +6,43 @@ import Theme from "./Theme"; * Represents a version of a theme in the application. * Tracks the theme id, version number, and release date. */ -class ThemeVersion extends Model { } +class ThemeVersion extends Model {} -ThemeVersion.init({ - // id to uniquely identify published theme version - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - // the identifier of the theme this version belongs to - theme_id: { - type: DataTypes.STRING, - allowNull: false, - references: { - model: Theme, - key: "id" - } - }, - // the version number of the theme - version: { - type: DataTypes.STRING, - allowNull: false - }, - // date when this version of the theme was released - created_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - } -}, { - sequelize, - modelName: "ThemeVersion", - timestamps: false -}); +ThemeVersion.init( + { + // id to uniquely identify published theme version + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + // the identifier of the theme this version belongs to + theme_id: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: Theme, + key: "id", + }, + }, + // the version number of the theme + version: { + type: DataTypes.STRING, + allowNull: false, + }, + // date when this version of the theme was released + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + }, + { + sequelize, + modelName: "ThemeVersion", + timestamps: false, + }, +); ThemeVersion.belongsTo(Theme, { foreignKey: "theme_id" }); -export default ThemeVersion; \ No newline at end of file +export default ThemeVersion; diff --git a/src/api/databases/sql/models/User.ts b/src/api/databases/sql/models/User.ts index 34df25e..c826574 100644 --- a/src/api/databases/sql/models/User.ts +++ b/src/api/databases/sql/models/User.ts @@ -1,51 +1,53 @@ import { DataTypes, Model } from "sequelize"; import { sequelize } from "../sql"; - /** * Stores the data of users. */ -class User extends Model { } +class User extends Model {} -User.init({ - // id to uniquely identify the user - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - // email, also uniquely identifies the user - email: { - type: DataTypes.STRING, - allowNull: false, - unique: true - }, - // the role of the user - role: { - type: DataTypes.ENUM("USER", "MODERATOR", "ADMIN"), - defaultValue: "USER", - allowNull: false - }, - // date when the user accepted the author agreement (necessary to publish themes/plugins) - accepted_author_agreement: { - type: DataTypes.DATE, - allowNull: true, - defaultValue: null // default to null which is not agreed yet - }, - // date when user was created - created_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - }, - // date when user was last updated - updated_at: { - type: DataTypes.DATE, - defaultValue: sequelize.literal("NOW()") - } -}, { - sequelize, - modelName: "User", - timestamps: false -}); +User.init( + { + // id to uniquely identify the user + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + // email, also uniquely identifies the user + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + // the role of the user + role: { + type: DataTypes.ENUM("USER", "MODERATOR", "ADMIN"), + defaultValue: "USER", + allowNull: false, + }, + // date when the user accepted the author agreement (necessary to publish themes/plugins) + accepted_author_agreement: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: null, // default to null which is not agreed yet + }, + // date when user was created + created_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + // date when user was last updated + updated_at: { + type: DataTypes.DATE, + defaultValue: sequelize.literal("NOW()"), + }, + }, + { + sequelize, + modelName: "User", + timestamps: false, + }, +); -export default User; \ No newline at end of file +export default User; diff --git a/src/api/databases/sql/models/UserRefreshToken.ts b/src/api/databases/sql/models/UserRefreshToken.ts index 13fa663..2e7b4d1 100644 --- a/src/api/databases/sql/models/UserRefreshToken.ts +++ b/src/api/databases/sql/models/UserRefreshToken.ts @@ -5,34 +5,37 @@ import User from "./User"; /** * Stores the refresh token for users. */ -class UserRefreshToken extends Model { } +class UserRefreshToken extends Model {} -UserRefreshToken.init({ - // user id to identify who the refresh token belongs to - user_id: { - type: DataTypes.UUID, - allowNull: false, - references: { - model: User, - key: "id" - }, - onDelete: "CASCADE", - primaryKey: true - }, - // actual refresh token - refresh_token: { - type: DataTypes.STRING, - allowNull: false - }, - // date when refresh token expires - expiry_date: { - type: DataTypes.DATE, - allowNull: false - } -}, { - sequelize, - modelName: "UserRefreshToken", - timestamps: false -}); +UserRefreshToken.init( + { + // user id to identify who the refresh token belongs to + user_id: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: User, + key: "id", + }, + onDelete: "CASCADE", + primaryKey: true, + }, + // actual refresh token + refresh_token: { + type: DataTypes.STRING, + allowNull: false, + }, + // date when refresh token expires + expiry_date: { + type: DataTypes.DATE, + allowNull: false, + }, + }, + { + sequelize, + modelName: "UserRefreshToken", + timestamps: false, + }, +); export default UserRefreshToken; diff --git a/src/api/databases/sql/sql.ts b/src/api/databases/sql/sql.ts index 304ee9f..f06dbd1 100644 --- a/src/api/databases/sql/sql.ts +++ b/src/api/databases/sql/sql.ts @@ -2,36 +2,35 @@ import { Sequelize } from "sequelize"; // setup sequelize with provided parameters const sequelize = new Sequelize({ - dialect: "mysql", - host: "mysql", - port: parseInt(process.env.MYSQL_PORT as string, 10) || 3306, - username: process.env.MYSQL_USER || "", - password: process.env.MYSQL_PASSWORD || "", - database: process.env.MYSQL_DATABASE || "" + dialect: "mysql", + host: "mysql", + port: parseInt(process.env.MYSQL_PORT as string, 10) || 3306, + username: process.env.MYSQL_USER || "", + password: process.env.MYSQL_PASSWORD || "", + database: process.env.MYSQL_DATABASE || "", }); // initialize databases const initializeDatabase = async () => { - try { - // connect to the database - await sequelize.authenticate(); - console.info("Connection to the database has been established successfully."); + try { + // connect to the database + await sequelize.authenticate(); + console.info( + "Connection to the database has been established successfully.", + ); - // a primary instance is assigned to alter tables if necessary in development/playground - // not ideal, but works and good enough for now - if (process.env.NODE_ENV !== "production" && process.env.IS_PRIMARY) { - await sequelize.sync({ alter: true }); - } else { - await sequelize.sync(); - } + // a primary instance is assigned to alter tables if necessary in development/playground + // not ideal, but works and good enough for now + if (process.env.NODE_ENV !== "production" && process.env.IS_PRIMARY) { + await sequelize.sync({ alter: true }); + } else { + await sequelize.sync(); + } - console.info("All models were synchronized successfully."); - } catch (error) { - console.error("Unable to connect to the database:", error); - } + console.info("All models were synchronized successfully."); + } catch (error) { + console.error("Unable to connect to the database:", error); + } }; -export { - initializeDatabase, - sequelize -}; +export { initializeDatabase, sequelize }; diff --git a/src/api/index.ts b/src/api/index.ts index 700ca97..4fc4e1e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -21,7 +21,7 @@ dotenv.config(); // enable express session debugging if not in prod if (process.env.NODE_ENV !== "production") { - process.env.DEBUG = "express-session"; + process.env.DEBUG = "express-session"; } // initialize database @@ -33,33 +33,40 @@ setUpMinioBucket(); const app = express(); // handle cors -app.use(cors({ - origin: process.env.FRONTEND_WEBSITE_URL, - credentials: true -})); +app.use( + cors({ + origin: process.env.FRONTEND_WEBSITE_URL, + credentials: true, + }), +); app.use(bodyParser.json()); // needed to ensure correct protocol due to nginx proxies app.set("trust proxy", true); // handles user session -app.use(session({ - store: redisSessionStore, - secret: process.env.SESSION_SECRET as string, - resave: false, - saveUninitialized: true, - cookie: { - httpOnly: true, - // if developing locally, set to insecure - secure: process.env.NODE_ENV !== "development", - // in production, use "lax" as frontend and backend have the same root domain - sameSite: process.env.NODE_ENV === "production" ? "lax" : "none", - // if not in production, leave domain as undefined - domain: process.env.NODE_ENV === "production" ? process.env.FRONTEND_WEBSITE_DOMAIN : undefined, - // expire after 3 months (milliseconds) - maxAge: 7776000000 - }, -})); +app.use( + session({ + store: redisSessionStore, + secret: process.env.SESSION_SECRET as string, + resave: false, + saveUninitialized: true, + cookie: { + httpOnly: true, + // if developing locally, set to insecure + secure: process.env.NODE_ENV !== "development", + // in production, use "lax" as frontend and backend have the same root domain + sameSite: process.env.NODE_ENV === "production" ? "lax" : "none", + // if not in production, leave domain as undefined + domain: + process.env.NODE_ENV === "production" + ? process.env.FRONTEND_WEBSITE_DOMAIN + : undefined, + // expire after 3 months (milliseconds) + maxAge: 7776000000, + }, + }), +); // handle routes const API_PREFIX = `/api/${process.env.API_VERSION}`; @@ -69,33 +76,40 @@ app.use(`${API_PREFIX}/users`, userRoutes); // load the swagger docs only if not in production if (process.env.NODE_ENV !== "production") { - const tsFilesInDir = fs.readdirSync(path.join(__dirname, "./swagger")).filter(file => path.extname(file) === ".js"); - let result = {}; - - const loadSwaggerFiles = async () => { - for (const file of tsFilesInDir) { - const filePath = path.join(__dirname, "./swagger", file); - const fileData = await import(filePath); - result = { ...result, ...fileData.default }; - } - - (swaggerDocument as any).paths = result; - - app.use("/api-docs", (req: any, res: any, next: any) => { - req.swaggerDoc = swaggerDocument; - next(); - }, swaggerUi.serveFiles(swaggerDocument), swaggerUi.setup()); - - console.info(`Swagger docs loaded.`); - }; - - loadSwaggerFiles(); + const tsFilesInDir = fs + .readdirSync(path.join(__dirname, "./swagger")) + .filter((file) => path.extname(file) === ".js"); + let result = {}; + + const loadSwaggerFiles = async () => { + for (const file of tsFilesInDir) { + const filePath = path.join(__dirname, "./swagger", file); + const fileData = await import(filePath); + result = { ...result, ...fileData.default }; + } + + (swaggerDocument as any).paths = result; + + app.use( + "/api-docs", + (req: any, res: any, next: any) => { + req.swaggerDoc = swaggerDocument; + next(); + }, + swaggerUi.serveFiles(swaggerDocument), + swaggerUi.setup(), + ); + + console.info(`Swagger docs loaded.`); + }; + + loadSwaggerFiles(); } else { - console.info("Swagger docs are disabled in production."); + console.info("Swagger docs are disabled in production."); } // start server, default to port 3000 if not specified const PORT = process.env.PORT || 3000; app.listen(PORT, () => { - console.info(`Server is running on port ${PORT}`); -}); \ No newline at end of file + console.info(`Server is running on port ${PORT}`); +}); diff --git a/src/api/interfaces/GitHubRepoContent.ts b/src/api/interfaces/GitHubRepoContent.ts index 7b7eb6f..a5a017d 100644 --- a/src/api/interfaces/GitHubRepoContent.ts +++ b/src/api/interfaces/GitHubRepoContent.ts @@ -1,22 +1,20 @@ // contents retrieved from github repo api // todo: check if fields are all present interface GitHubRepoContent { - name: string; - path: string; - sha: string; - size: number; - url: string; - html_url: string; - git_url: string; - download_url: string | null; - type: string; - _links: { - self: string; - git: string; - html: string; - }; + name: string; + path: string; + sha: string; + size: number; + url: string; + html_url: string; + git_url: string; + download_url: string | null; + type: string; + _links: { + self: string; + git: string; + html: string; + }; } -export { - GitHubRepoContent -}; +export { GitHubRepoContent }; diff --git a/src/api/interfaces/ThemeMetaData.ts b/src/api/interfaces/ThemeMetaData.ts index 8fa6336..2516f82 100644 --- a/src/api/interfaces/ThemeMetaData.ts +++ b/src/api/interfaces/ThemeMetaData.ts @@ -1,13 +1,11 @@ // contents retrieved from theme meta data (i.e. github meta.json) interface ThemeMetaData { - name: string; - description: string; - author: string; - github: string; - tags: string[]; - version: string; + name: string; + description: string; + author: string; + github: string; + tags: string[]; + version: string; } -export { - ThemeMetaData -}; +export { ThemeMetaData }; diff --git a/src/api/interfaces/TokenResponse.ts b/src/api/interfaces/TokenResponse.ts index f2437fa..d1724ff 100644 --- a/src/api/interfaces/TokenResponse.ts +++ b/src/api/interfaces/TokenResponse.ts @@ -1,11 +1,9 @@ // token information consolidated from provider interface TokenResponse { - access_token: string; - access_token_expiry: number; - refresh_token: string; - refresh_token_expiry: number; + access_token: string; + access_token_expiry: number; + refresh_token: string; + refresh_token_expiry: number; } -export { - TokenResponse -}; +export { TokenResponse }; diff --git a/src/api/interfaces/UserData.ts b/src/api/interfaces/UserData.ts index 12f4a1c..6100263 100644 --- a/src/api/interfaces/UserData.ts +++ b/src/api/interfaces/UserData.ts @@ -1,18 +1,16 @@ // user data persisted for a session, similar to user provider data but contains additional id and role fields interface UserData { - id: string; - role: string; - name: string; - email: string; - handle: string; - avatar_url: string; - status: string; - location: string; - profile_url: string; - provider: string; - provider_user_id: string; + id: string; + role: string; + name: string; + email: string; + handle: string; + avatar_url: string; + status: string; + location: string; + profile_url: string; + provider: string; + provider_user_id: string; } -export { - UserData -}; +export { UserData }; diff --git a/src/api/interfaces/UserProviderData.ts b/src/api/interfaces/UserProviderData.ts index 6e7aa63..d8e8f66 100644 --- a/src/api/interfaces/UserProviderData.ts +++ b/src/api/interfaces/UserProviderData.ts @@ -1,16 +1,14 @@ // common user data retrieved from providers interface UserProviderData { - name: string; - email: string; - handle: string; - avatar_url: string; - status: string; - location: string; - profile_url: string; - provider: string; - provider_user_id: string; + name: string; + email: string; + handle: string; + avatar_url: string; + status: string; + location: string; + profile_url: string; + provider: string; + provider_user_id: string; } -export { - UserProviderData -}; +export { UserProviderData }; diff --git a/src/api/middleware/userSessionMiddleware.ts b/src/api/middleware/userSessionMiddleware.ts index 3554da0..a5760e5 100644 --- a/src/api/middleware/userSessionMiddleware.ts +++ b/src/api/middleware/userSessionMiddleware.ts @@ -3,22 +3,30 @@ import { getUserData } from "../services/authentication/authentication"; /** * Checks if an existing user session exists and if does, attach user data. - * + * * @param req request from call * @param res response to call * @param next next to proceed - * + * * @returns 403 if session not found, else proceed */ -const checkUserSession = async (req: Request, res: Response, next: NextFunction) => { - const userData = await getUserData(req.sessionID, req.session.userId || null, req.session.provider as string); +const checkUserSession = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + const userData = await getUserData( + req.sessionID, + req.session.userId || null, + req.session.provider as string, + ); - if (!userData) { - return res.status(401).json({ error: "User session not found" }); - } + if (!userData) { + return res.status(401).json({ error: "User session not found" }); + } - req.userData = userData; - next(); + req.userData = userData; + next(); }; -export default checkUserSession; \ No newline at end of file +export default checkUserSession; diff --git a/src/api/routes/authRoutes.ts b/src/api/routes/authRoutes.ts index 72d456a..e073059 100644 --- a/src/api/routes/authRoutes.ts +++ b/src/api/routes/authRoutes.ts @@ -1,7 +1,7 @@ import express from "express"; import { - handleCallback, - handleLoginProcess, + handleCallback, + handleLoginProcess, } from "../controllers/authController"; const router = express.Router(); diff --git a/src/api/routes/themeRoutes.ts b/src/api/routes/themeRoutes.ts index 7e4d518..bfc9ae1 100644 --- a/src/api/routes/themeRoutes.ts +++ b/src/api/routes/themeRoutes.ts @@ -1,10 +1,10 @@ import express from "express"; import multer from "multer"; import { - getThemes, - getThemeVersions, - publishTheme, - unpublishTheme, + getThemes, + getThemeVersions, + publishTheme, + unpublishTheme, } from "../controllers/themeController"; import checkUserSession from "../middleware/userSessionMiddleware"; @@ -13,27 +13,29 @@ const storage = multer.memoryStorage(); // file upload middleware with file type filter and limits const upload = multer({ - storage: storage, - // todo: review this limit - limits: { - fileSize: 5 * 1024 * 1024, // default to 5mb - }, - fileFilter: (req, file, cb) => { - // allow only these file extensions - const allowedExtensions = [".css", ".json", ".png"]; - const fileExtension = getFileExtension(file.originalname); - // todo: can enforce file name together with extension as well - if (allowedExtensions.includes(fileExtension)) { - cb(null, true); - } else { - cb(new Error("Invalid file extension")); - } - } + storage: storage, + // todo: review this limit + limits: { + fileSize: 5 * 1024 * 1024, // default to 5mb + }, + fileFilter: (req, file, cb) => { + // allow only these file extensions + const allowedExtensions = [".css", ".json", ".png"]; + const fileExtension = getFileExtension(file.originalname); + // todo: can enforce file name together with extension as well + if (allowedExtensions.includes(fileExtension)) { + cb(null, true); + } else { + cb(new Error("Invalid file extension")); + } + }, }); // helper function to get file extension function getFileExtension(filename: string) { - return filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2).toLowerCase(); + return filename + .slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2) + .toLowerCase(); } const router = express.Router(); @@ -45,13 +47,18 @@ router.get("/", getThemes); router.get("/versions", getThemeVersions); // publish theme -router.post("/publish", checkUserSession, upload.fields([ - { name: "styles", maxCount: 1 }, - { name: "options", maxCount: 1 }, - { name: "display", maxCount: 1 } -]), publishTheme); +router.post( + "/publish", + checkUserSession, + upload.fields([ + { name: "styles", maxCount: 1 }, + { name: "options", maxCount: 1 }, + { name: "display", maxCount: 1 }, + ]), + publishTheme, +); // unpublish theme router.delete("/unpublish", checkUserSession, unpublishTheme); -export default router; \ No newline at end of file +export default router; diff --git a/src/api/routes/userRoutes.ts b/src/api/routes/userRoutes.ts index f8673b0..20d26af 100644 --- a/src/api/routes/userRoutes.ts +++ b/src/api/routes/userRoutes.ts @@ -1,10 +1,10 @@ import express from "express"; import { - addUserFavoriteTheme, - getUserFavoriteThemes, - getUserProfile, - getUserThemes, - removeUserFavoriteTheme + addUserFavoriteTheme, + getUserFavoriteThemes, + getUserProfile, + getUserThemes, + removeUserFavoriteTheme, } from "../controllers/userController"; import checkUserSession from "../middleware/userSessionMiddleware"; @@ -23,7 +23,7 @@ router.get("/themes/favorited", checkUserSession, getUserFavoriteThemes); router.post("/themes/favorited", checkUserSession, addUserFavoriteTheme); // unfavorites a theme for user -router.delete("/themes/favorited", checkUserSession, removeUserFavoriteTheme) +router.delete("/themes/favorited", checkUserSession, removeUserFavoriteTheme); // todo: add an endpoint for users to attempt to claim theme ownership // required if a theme is on github but the author has never logged @@ -31,4 +31,4 @@ router.delete("/themes/favorited", checkUserSession, removeUserFavoriteTheme) // since this means themes were directly added to github without going // through the website -export default router; \ No newline at end of file +export default router; diff --git a/src/api/services/authentication/authentication.ts b/src/api/services/authentication/authentication.ts index f47d28e..9c05a01 100644 --- a/src/api/services/authentication/authentication.ts +++ b/src/api/services/authentication/authentication.ts @@ -19,182 +19,207 @@ import * as GitHubProvider from "./providers/github"; * * @returns token response if successful, null otherwise */ -const fetchTokensWithCode = async (sessionId: string, key: string, provider: string): Promise