diff --git a/.commitlintrc.mjs b/.commitlintrc.mjs
new file mode 100644
index 0000000..e3610d9
--- /dev/null
+++ b/.commitlintrc.mjs
@@ -0,0 +1,30 @@
+const config = {
+ extends: ['@commitlint/config-conventional'],
+ rules: {
+ 'type-enum': [
+ 2,
+ 'always',
+ [
+ 'build',
+ 'chore',
+ 'config',
+ 'doc',
+ 'feat',
+ 'fix',
+ 'hotfix',
+ 'i18n',
+ 'refactor',
+ 'revert',
+ 'test',
+ 'ui',
+ 'wip',
+ 'publish',
+ 'docker',
+ 'WIP',
+ ],
+ ],
+ },
+ ignores: [(message) => message.includes('WIP'), (message) => message.includes('wip')],
+};
+
+export default config;
diff --git a/.env.sample b/.env.sample
index b6220df..a2e1c7c 100644
--- a/.env.sample
+++ b/.env.sample
@@ -23,8 +23,6 @@ CONFIG_PATH=./config
SSH_PATH=./ssh
SSH_HOST=./ssh_host
BORG_REPOSITORY_PATH=./repos
-TMP_PATH=./tmp
-LOGS_PATH=./logs
## Optional variables section ##
@@ -32,10 +30,22 @@ LOGS_PATH=./logs
FQDN_LAN=
SSH_SERVER_PORT_LAN=
+# Disable the DELETE feature
+#DISABLE_DELETE_REPO=true
+
+# Disable the integrations (API tokens to CRUD repositories)
+#DISABLE_INTEGRATIONS=true
+
+# Hide the SSH port in the UI : quickcommands & wizard
+#HIDE_SSH_PORT=true
+
# SMTP server settings
MAIL_SMTP_FROM=
MAIL_SMTP_HOST=
MAIL_SMTP_PORT=
MAIL_SMTP_LOGIN=
MAIL_SMTP_PWD=
-MAIL_REJECT_SELFSIGNED_TLS=
\ No newline at end of file
+MAIL_REJECT_SELFSIGNED_TLS=
+
+# Force app to start on IPv6
+#HOSTNAME=::
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..136334e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,35 @@
+---
+name: Bug report
+about: Create a report a bug
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**BorgWarehouse version :**
+**Installation type :**
+- [ ] Docker
+- [ ] Baremetal (Debian/Ubuntu)
+- [ ] Other environment :
+
+-------
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Additional context**
+Add any other context about the problem here.
+
+**Please, [BorgWarehouse's documentation](https://borgwarehouse.com/)
+ is up to date and comprehensive, so take the time to look for answers. You can also look for answers in the project's historical [github issues](https://github.com/Ravinou/borgwarehouse/issues?q=is%3Aissue%20state%3Aclosed). I take time to answer each issue, but it's always less time for BorgWarehouse development. Thanks in advance.**
diff --git a/.github/ISSUE_TEMPLATE/i-need-help.md b/.github/ISSUE_TEMPLATE/i-need-help.md
new file mode 100644
index 0000000..8aa78e6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/i-need-help.md
@@ -0,0 +1,21 @@
+---
+name: I need help
+about: You need help about installation, usage, or specific cases.
+title: ''
+labels: help wanted
+assignees: ''
+
+---
+
+**BorgWarehouse version :**
+**Installation type :**
+- [ ] Docker
+- [ ] Baremetal (Debian/Ubuntu)
+- [ ] Other environment :
+
+-------
+
+Describe your problem here.
+
+**Please, [BorgWarehouse's documentation](https://borgwarehouse.com/)
+ is up to date and comprehensive, so take the time to look for answers. You can also look for answers in the project's historical [github issues](https://github.com/Ravinou/borgwarehouse/issues?q=is%3Aissue%20state%3Aclosed). I take time to answer each issue, but it's always less time for BorgWarehouse development. Thanks in advance.**
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3110836..34a7437 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,16 +1,18 @@
version: 2
updates:
- - package-ecosystem: "docker"
- directory: "/"
+ - package-ecosystem: 'docker'
+ directory: '/'
schedule:
- interval: "daily"
- - package-ecosystem: "npm"
- directory: "/"
+ interval: 'daily'
+ # Note: Dependabot uses "npm" ecosystem but automatically detects pnpm-lock.yaml
+ # Make sure package-lock.json is gitignored to prevent confusion
+ - package-ecosystem: 'npm'
+ directory: '/'
schedule:
- interval: "daily"
+ interval: 'daily'
# Maintain dependencies for GitHub Actions
# src: https://github.com/marketplace/actions/build-and-push-docker-images#keep-up-to-date-with-github-dependabot
- - package-ecosystem: "github-actions"
- directory: "/"
+ - package-ecosystem: 'github-actions'
+ directory: '/'
schedule:
- interval: "daily"
+ interval: 'daily'
diff --git a/.github/workflows/bats.yml b/.github/workflows/bats.yml
new file mode 100644
index 0000000..825fd0d
--- /dev/null
+++ b/.github/workflows/bats.yml
@@ -0,0 +1,29 @@
+name: Bats
+
+permissions:
+ contents: read
+
+on:
+ push:
+ branches:
+ - main
+ - develop
+ pull_request:
+ branches:
+ - main
+ - develop
+jobs:
+ bats-test:
+ name: Run bats tests against shells
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build container & run bats tests
+ run: |
+ docker compose -f tests/bats/docker-compose.yml up --abort-on-container-exit --build
diff --git a/.github/workflows/docker-image-develop.yml b/.github/workflows/docker-image-develop.yml
index 80daaea..14c33a5 100644
--- a/.github/workflows/docker-image-develop.yml
+++ b/.github/workflows/docker-image-develop.yml
@@ -1,29 +1,38 @@
name: Build and Push Docker Image for Develop Branch
on:
- push:
- branches:
- - 'develop'
+ push:
+ branches:
+ - 'develop'
+
+permissions:
+ contents: read
jobs:
- docker:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
- - name: Login to Docker Hub
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: Build and push
- uses: docker/build-push-action@v5
- with:
- context: .
- push: true
- platforms: linux/amd64,linux/arm64 # linux/arm/v7 arm32 is not supported by node20 https://github.com/nodejs/docker-node/issues/1946
- tags: borgwarehouse/borgwarehouse:develop
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ - name: Update package.json version
+ run: |
+ COMMIT=$(git rev-parse --short HEAD)
+ echo "Current Commit: $COMMIT"
+ jq '.version = "develop-'$COMMIT'"' package.json > package.tmp.json
+ mv package.tmp.json package.json
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ platforms: linux/amd64,linux/arm64 # linux/arm/v7 arm32 is not supported by node20 https://github.com/nodejs/docker-node/issues/1946
+ tags: borgwarehouse/borgwarehouse:develop
diff --git a/.github/workflows/docker-image-latest.yml b/.github/workflows/docker-image-latest.yml
index f273b46..7957cb8 100644
--- a/.github/workflows/docker-image-latest.yml
+++ b/.github/workflows/docker-image-latest.yml
@@ -1,4 +1,6 @@
name: Build and Push Docker Image
+permissions:
+ contents: read
on:
push:
@@ -10,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
@@ -21,7 +23,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: .
push: true
diff --git a/.github/workflows/docker-image-release.yml b/.github/workflows/docker-image-release.yml
index 26dd466..01f4eb5 100644
--- a/.github/workflows/docker-image-release.yml
+++ b/.github/workflows/docker-image-release.yml
@@ -5,12 +5,15 @@ on:
types:
- published
+permissions:
+ contents: read
+
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
@@ -24,7 +27,7 @@ jobs:
id: get_release_tag
run: echo "::set-output name=TAG::${{ github.event.release.tag_name }}"
- name: Build and push
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
context: .
push: true
diff --git a/.github/workflows/docker-image-test.yml b/.github/workflows/docker-image-test.yml
index 448f45c..4d716ad 100644
--- a/.github/workflows/docker-image-test.yml
+++ b/.github/workflows/docker-image-test.yml
@@ -1,21 +1,24 @@
-name: Test Docker Container Build on Pull Request
+name: Test to build docker container on Pull Request
+
+permissions:
+ contents: read
on:
- pull_request:
- branches:
- - main
- - develop
+ pull_request:
+ branches:
+ - main
+ - develop
jobs:
- build-container:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
- - name: Build Docker Container
- run: |
- docker buildx build --platform linux/amd64,linux/arm64 -t borgwarehouse:pr-${{ github.event.pull_request.number }} .
+ build-container:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Build BorgWarehouse Container
+ run: |
+ docker buildx build --platform linux/amd64,linux/arm64 -t borgwarehouse:pr-${{ github.event.pull_request.number }} .
diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml
index 92522ae..8b954af 100644
--- a/.github/workflows/shellcheck.yml
+++ b/.github/workflows/shellcheck.yml
@@ -4,19 +4,21 @@ on:
- main
- develop
pull_request:
- branches: main
+ branches:
+ - main
+ - develop
-name: "Shellcheck"
+name: 'Shellcheck'
permissions: {}
jobs:
shellcheck:
name: Shellcheck
runs-on: ubuntu-latest
-
+
steps:
- - uses: actions/checkout@v4
-
+ - uses: actions/checkout@v6
+
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
env:
diff --git a/.github/workflows/vitest.yml b/.github/workflows/vitest.yml
new file mode 100644
index 0000000..fe9d512
--- /dev/null
+++ b/.github/workflows/vitest.yml
@@ -0,0 +1,63 @@
+name: Vitest & ESLint CI
+
+on:
+ push:
+ branches:
+ - main
+ - develop
+ pull_request:
+ branches:
+ - main
+ - develop
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Run Vitest
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 22
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Run Vitest
+ run: pnpm run test
+
+ lint:
+ name: Run ESLint
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 22
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Run ESLint
+ run: pnpm exec eslint
diff --git a/.gitignore b/.gitignore
index 2373057..c95738d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,6 +50,14 @@ typings/
# Optional npm cache directory
.npm
+# pnpm
+.pnpm-store/
+pnpm-debug.log*
+
+# Lock files (pnpm-lock.yaml is used)
+package-lock.json
+yarn.lock
+
# Optional eslint cache
.eslintcache
@@ -111,4 +119,7 @@ config/repo.json
config/users.json
# docker files
-docker-compose.yml
\ No newline at end of file
+docker-compose.yml
+
+# Commit tests docker-compose
+!tests/bats/docker-compose.yml
\ No newline at end of file
diff --git a/.husky/append-icon.sh b/.husky/append-icon.sh
new file mode 100755
index 0000000..a54594a
--- /dev/null
+++ b/.husky/append-icon.sh
@@ -0,0 +1,92 @@
+#!/bin/bash
+
+# define log prefix
+prefix="pre-commit:"
+
+# store message file, first and only param of hook
+commitMessageFile="$1"
+
+# breaking change icon !
+boomIcon=':boom:'
+
+# check for breaking change in file content
+# find any line starting with 'BREAKING CHANGE'
+function checkBreakingChangeInBody() {
+ breakingChange='BREAKING CHANGE'
+ while read -r line; do
+ if [[ "$line" == "$breakingChange"* ]]; then
+ echo "$prefix found $breakingChange in message body"
+ return 0
+ fi
+ done < "$1"
+ return 1
+}
+
+function findTypeIcon() {
+ message="$1"
+
+ if [[ "$message" =~ ^.*!:\ .* ]]; then
+ echo "$boomIcon"
+ return 0
+ fi
+
+ declare -A icons=(
+ [build]='๐ค'
+ [chore]='๐งน'
+ ["chore(deps)"]='๐งน'
+ [config]='๐ง'
+ [deploy]='๐'
+ [doc]='๐'
+ [feat]='โจ'
+ [fix]='๐'
+ [hotfix]='๐'
+ [i18n]='๐ฌ'
+ [publish]='๐ฆ'
+ [refactor]='โก'
+ [revert]='โช'
+ [test]='โ
'
+ [ui]='๐จ'
+ [wip]='๐ง'
+ [WIP]='๐ง'
+ [docker]='๐ณ'
+ )
+
+ commit_type="${message%%:*}"
+
+ icon="${icons[$commit_type]}"
+ if [[ -n "$icon" ]]; then
+ echo "$icon"
+ return 0
+ else
+ return 1
+ fi
+}
+
+# extract original message from the first line of file
+message=$(head -n 1 <"$commitMessageFile")
+echo "$prefix commit subject: '$message'"
+
+if checkBreakingChangeInBody "$commitMessageFile"; then
+ echo 'setting breaking change icon'
+ icon=$boomIcon
+else
+ icon=$(findTypeIcon "$message")
+ if [ $? -eq 1 ]; then
+ echo "$prefix โ unable to find icon corresponding to commit type. Make sure your commit-lint config (.commitlintrc.js) and append-msg script (append-msg.sh) types match"
+ exit 1
+ fi
+fi
+
+# check if icon has been appended before
+if [[ "$message" == *"$icon"* ]]; then
+ echo "โญ๏ธ skipping icon append as it's been added before"
+ exit 0
+fi
+
+# otherwise append icon
+updatedMessage="${message/:/: $icon}"
+
+# replace first line of file with updated message
+sed -i "1s/.*/$updatedMessage/" "$commitMessageFile"
+
+echo "$prefix โ
appended icon $icon to commit message subject"
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100755
index 0000000..993e036
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1,5 @@
+# run commit lint
+npx commitlint --edit "$1"
+
+# run script to prepend message with icon
+./.husky/append-icon.sh "$1"
\ No newline at end of file
diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg
new file mode 100755
index 0000000..d48435c
--- /dev/null
+++ b/.husky/prepare-commit-msg
@@ -0,0 +1,5 @@
+# Check if it's an amend commit
+if [ "$2" = "commit" ]; then
+ echo "Amendment detected, appending icon..."
+ ./.husky/append-icon.sh "$1"
+fi
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..cbcc181
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,7 @@
+# Configuration pnpm
+auto-install-peers=true
+strict-peer-dependencies=false
+shamefully-hoist=false
+
+# Force pnpm usage (prevent npm/yarn)
+package-manager=pnpm
diff --git a/.prettierrc.json b/.prettierrc.json
index d4fb785..a7c797f 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -1,21 +1,20 @@
{
- "trailingComma": "es5",
- "tabWidth": 4,
- "semi": true,
- "singleQuote": true,
- "arrowParens": "always",
- "bracketSpacing": true,
- "endOfLine": "lf",
- "htmlWhitespaceSensitivity": "css",
- "insertPragma": false,
- "singleAttributePerLine": false,
- "bracketSameLine": false,
- "jsxBracketSameLine": false,
- "jsxSingleQuote": true,
- "printWidth": 80,
- "proseWrap": "preserve",
- "quoteProps": "as-needed",
- "requirePragma": false,
- "useTabs": false,
- "embeddedLanguageFormatting": "auto"
+ "trailingComma": "es5",
+ "tabWidth": 2,
+ "semi": true,
+ "singleQuote": true,
+ "arrowParens": "always",
+ "bracketSpacing": true,
+ "endOfLine": "lf",
+ "htmlWhitespaceSensitivity": "css",
+ "insertPragma": false,
+ "singleAttributePerLine": false,
+ "bracketSameLine": false,
+ "jsxSingleQuote": true,
+ "printWidth": 100,
+ "proseWrap": "preserve",
+ "quoteProps": "as-needed",
+ "requirePragma": false,
+ "useTabs": false,
+ "embeddedLanguageFormatting": "auto"
}
diff --git a/Components/Repo/QuickCommands/QuickCommands.js b/Components/Repo/QuickCommands/QuickCommands.js
deleted file mode 100644
index c76a3a7..0000000
--- a/Components/Repo/QuickCommands/QuickCommands.js
+++ /dev/null
@@ -1,70 +0,0 @@
-//Lib
-import React from 'react';
-import { useState } from 'react';
-import classes from './QuickCommands.module.css';
-import { IconSettingsAutomation, IconCopy } from '@tabler/icons-react';
-
-export default function QuickCommands(props) {
- ////Vars
- const wizardEnv = props.wizardEnv;
- //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
- let FQDN;
- let SSH_SERVER_PORT;
- if (
- props.lanCommand &&
- wizardEnv.FQDN_LAN &&
- wizardEnv.SSH_SERVER_PORT_LAN
- ) {
- FQDN = wizardEnv.FQDN_LAN;
- SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT_LAN;
- } else {
- FQDN = wizardEnv.FQDN;
- SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT;
- }
-
- //State
- const [isCopied, setIsCopied] = useState(false);
-
- //Functions
- const handleCopy = async () => {
- // Asynchronously call copy to clipboard
- navigator.clipboard
- .writeText(
- `ssh://${wizardEnv.UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.repositoryName}`
- )
- .then(() => {
- // If successful, update the isCopied state value
- setIsCopied(true);
- setTimeout(() => {
- setIsCopied(false);
- }, 1500);
- })
- .catch((err) => {
- console.log(err);
- });
- };
-
- return (
-
- {isCopied ? (
-
Copied !
- ) : (
-
- ssh://{wizardEnv.UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
- {props.repositoryName}
-
- )}
-
- {props.lanCommand &&
LAN
}
-
-
-
- );
-}
diff --git a/Components/Repo/QuickCommands/QuickCommands.module.css b/Components/Repo/QuickCommands/QuickCommands.module.css
index 13ce01f..77cc329 100644
--- a/Components/Repo/QuickCommands/QuickCommands.module.css
+++ b/Components/Repo/QuickCommands/QuickCommands.module.css
@@ -1,116 +1,117 @@
.container {
- display: flex;
- align-items: center;
- align-self: flex-start;
- margin: auto 47px auto auto;
+ display: flex;
+ align-items: center;
+ align-self: flex-start;
+ margin: auto 25px auto auto;
}
.icons {
- position: relative;
- bottom: 13px;
+ position: relative;
+ bottom: 13px;
}
.quickSetting {
- position: absolute;
- visibility: visible;
- opacity: 1;
+ position: absolute;
+ visibility: visible;
+ opacity: 1;
}
.lanBadge {
- border-radius: 5px;
- border: 1px solid #6d4aff;
- color: #6d4aff;
- font-size: 0.9em;
- padding: 2px 5px;
- margin-right: 8px;
+ border-radius: 5px;
+ border: 1px solid #6d4aff;
+ color: #6d4aff;
+ font-size: 0.9em;
+ padding: 2px 5px;
+ margin-right: 8px;
}
.tooltip {
- visibility: hidden;
- opacity: 0;
- width: 100%;
- height: 100%;
- border: 1px solid #6d4aff21;
- background-color: #f5f5f5;
- border-radius: 5px;
- box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset;
- color: #65748b;
- font-size: 0.95rem;
- padding: 5px 5px;
- transition: 0.5s opacity;
+ visibility: hidden;
+ opacity: 0;
+ width: 100%;
+ height: 100%;
+ border: 1px solid #6d4aff21;
+ background-color: #fafafa;
+ border-radius: 5px;
+ box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset;
+ color: #65748b;
+ font-size: 0.95rem;
+ padding: 5px 5px;
+ transition: 0.5s opacity;
}
.copyButton {
- position: absolute;
- visibility: hidden;
- opacity: 0;
- border: none;
- background-color: none;
+ position: absolute;
+ visibility: hidden;
+ opacity: 0;
+ border: none;
+ background-color: none;
}
.copyValid {
- margin: auto 8px auto auto;
- font-size: 0.95rem;
- color: #6d4aff;
- animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
+ margin: auto 8px auto auto;
+ padding: 6px 6px;
+ font-size: 0.95rem;
+ color: #6d4aff;
+ animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
@keyframes scale-in-center {
- 0% {
- -webkit-transform: scale(0);
- transform: scale(0);
- opacity: 1;
- }
- 100% {
- -webkit-transform: scale(1);
- transform: scale(1);
- opacity: 1;
- }
+ 0% {
+ -webkit-transform: scale(0);
+ transform: scale(0);
+ opacity: 1;
+ }
+ 100% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ opacity: 1;
+ }
}
/* On Hover */
.container:hover .tooltip {
- visibility: visible;
- opacity: 1;
- width: 100%;
- height: 100%;
- border: 1px solid #6d4aff21;
- background-color: #f5f5f5;
- border-radius: 5px;
- box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset;
- color: #65748b;
- font-size: 0.95rem;
- padding: 5px 5px;
- transition: 0.5s opacity;
+ visibility: visible;
+ opacity: 1;
+ width: 100%;
+ height: 100%;
+ border: 1px solid #6d4aff21;
+ background-color: #fafafa;
+ border-radius: 5px;
+ box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset;
+ color: #65748b;
+ font-size: 0.95rem;
+ padding: 5px 5px;
+ transition: 0.5s opacity;
}
.container:hover .copyButton {
- position: absolute;
- visibility: visible;
- opacity: 1;
- border: none;
- background-color: transparent;
- cursor: pointer;
+ position: absolute;
+ visibility: visible;
+ opacity: 1;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
}
.container:hover .quickSetting {
- position: absolute;
- visibility: hidden;
- opacity: 0;
+ position: absolute;
+ visibility: hidden;
+ opacity: 0;
}
.container:hover .lanBadge {
- visibility: hidden;
- opacity: 0;
- width: 0;
- height: 0;
- margin: 0;
- padding: 0;
+ visibility: hidden;
+ opacity: 0;
+ width: 0;
+ height: 0;
+ margin: 0;
+ padding: 0;
}
@media all and (max-width: 1000px) {
- .container {
- display: none;
- }
+ .container {
+ display: none;
+ }
}
diff --git a/Components/Repo/QuickCommands/QuickCommands.tsx b/Components/Repo/QuickCommands/QuickCommands.tsx
new file mode 100644
index 0000000..8fe57b3
--- /dev/null
+++ b/Components/Repo/QuickCommands/QuickCommands.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { useState } from 'react';
+import classes from './QuickCommands.module.css';
+import { IconSettingsAutomation, IconCopy } from '@tabler/icons-react';
+import { lanCommandOption } from '~/helpers/functions';
+import { WizardEnvType } from '~/types/domain/config.types';
+
+type QuickCommandsProps = {
+ repositoryName: string;
+ wizardEnv?: WizardEnvType;
+ lanCommand?: boolean;
+};
+
+export default function QuickCommands(props: QuickCommandsProps) {
+ const wizardEnv = props.wizardEnv;
+ //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
+ const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.lanCommand);
+
+ const [isCopied, setIsCopied] = useState(false);
+
+ const handleCopy = async () => {
+ // Asynchronously call copy to clipboard
+ navigator.clipboard
+ .writeText(
+ `ssh://${wizardEnv?.UNIX_USER}@${FQDN}${SSH_SERVER_PORT ? SSH_SERVER_PORT : ''}/./${props.repositoryName}`
+ )
+ .then(() => {
+ setIsCopied(true);
+ setTimeout(() => {
+ setIsCopied(false);
+ }, 1500);
+ })
+ .catch((err) => {
+ console.log(err);
+ });
+ };
+
+ return (
+
+ {isCopied ? (
+
Copied !
+ ) : (
+
+ ssh://{wizardEnv?.UNIX_USER}@{FQDN}
+ {SSH_SERVER_PORT ? SSH_SERVER_PORT : ''}/./
+ {props.repositoryName}
+
+ )}
+
+ {props.lanCommand &&
LAN
}
+
+
+
+ );
+}
diff --git a/Components/Repo/Repo.js b/Components/Repo/Repo.js
deleted file mode 100644
index 2dce1ae..0000000
--- a/Components/Repo/Repo.js
+++ /dev/null
@@ -1,217 +0,0 @@
-//Lib
-import { useState } from 'react';
-import classes from './Repo.module.css';
-import {
- IconSettings,
- IconInfoCircle,
- IconChevronDown,
- IconChevronUp,
- IconBellOff,
- IconLockPlus,
-} from '@tabler/icons-react';
-import timestampConverter from '../../helpers/functions/timestampConverter';
-import StorageBar from '../UI/StorageBar/StorageBar';
-import QuickCommands from './QuickCommands/QuickCommands';
-
-export default function Repo(props) {
- //Load displayDetails from LocalStorage
- const displayDetailsFromLS = () => {
- try {
- if (
- localStorage.getItem('displayDetailsRepo' + props.id) === null
- ) {
- localStorage.setItem(
- 'displayDetailsRepo' + props.id,
- JSON.stringify(true)
- );
- return true;
- } else {
- return JSON.parse(
- localStorage.getItem('displayDetailsRepo' + props.id)
- );
- }
- } catch (error) {
- console.log(
- 'LocalStorage error, key',
- 'displayDetailsRepo' + props.id,
- 'will be removed. Try again.',
- 'Error message on this key : ',
- error
- );
- localStorage.removeItem('displayDetailsRepo' + props.id);
- }
- };
-
- //States
- const [displayDetails, setDisplayDetails] = useState(displayDetailsFromLS);
-
- //BUTTON : Display or not repo details for ONE repo
- const displayDetailsForOneHandler = (boolean) => {
- //Update localStorage
- localStorage.setItem(
- 'displayDetailsRepo' + props.id,
- JSON.stringify(boolean)
- );
- setDisplayDetails(boolean);
- };
-
- //Status indicator
- const statusIndicator = () => {
- return props.status
- ? classes.statusIndicatorGreen
- : classes.statusIndicatorRed;
- };
-
- //Alert indicator
- const alertIndicator = () => {
- if (props.alert === 0) {
- return (
-
-
-
- );
- }
- };
-
- const appendOnlyModeIndicator = () => {
- if (props.appendOnlyMode) {
- return (
-
-
-
- );
- }
- };
-
- return (
- <>
- {displayDetails ? (
- <>
-
-
-
-
{props.alias}
- {appendOnlyModeIndicator()}
- {alertIndicator()}
- {props.comment && (
-
-
-
- {props.comment}
-
-
- )}
-
-
-
-
-
-
- Repository
-
- Storage Size
-
-
- Storage Used
-
-
- Last change
-
- ID
- Edit
-
-
-
-
- {props.repositoryName}
- {props.storageSize} GB
-
-
-
-
-
- {props.lastSave === 0
- ? '-'
- : timestampConverter(
- props.lastSave
- )}
-
-
- #{props.id}
-
-
-
- props.repoManageEditHandler()
- }
- />
-
-
-
-
-
-
- >
- ) : (
- <>
-
-
-
-
{props.alias}
- {appendOnlyModeIndicator()}
- {alertIndicator()}
- {props.comment && (
-
-
-
- {props.comment}
-
-
- )}
-
-
- {props.lastSave === 0
- ? null
- : timestampConverter(props.lastSave)}
-
- #{props.id}
-
-
-
- >
- )}
- {displayDetails ? (
-
- {
- displayDetailsForOneHandler(false);
- }}
- />
-
- ) : (
-
- {
- displayDetailsForOneHandler(true);
- }}
- />
-
- )}
- >
- );
-}
diff --git a/Components/Repo/Repo.module.css b/Components/Repo/Repo.module.css
index edbf05e..8311137 100644
--- a/Components/Repo/Repo.module.css
+++ b/Components/Repo/Repo.module.css
@@ -1,250 +1,309 @@
/*Repo CLOSE*/
.RepoClose {
- display: flex;
- justify-content: space-between;
- align-items: center;
- box-shadow:
- 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 1px 2px rgba(0, 0, 0, 0.24);
- width: auto;
- max-height: 65px;
- margin: 20px 0px 0px 0px;
- border-radius: 5px;
- overflow: visible;
- /* Need to display comment on hover (which is position : absolute) */
- position: relative;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-shadow:
+ 0 1px 3px rgba(0, 0, 0, 0.12),
+ 0 1px 2px rgba(0, 0, 0, 0.24);
+ width: auto;
+ max-height: 65px;
+ margin: 20px 0px 0px 0px;
+ border-radius: 5px;
+ overflow: visible;
+ /* Need to display comment on hover (which is position : absolute) */
+ position: relative;
+ background: #fff;
}
.closeFlex {
- display: flex;
- align-items: center;
- padding: 15px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 15px;
+ gap: 10px;
}
.RepoClose .lastSave {
- padding: 15px;
+ white-space: nowrap;
+}
+
+.RepoClose .leftGroup {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ min-width: 0;
+ gap: 10px;
+}
+
+.RepoClose .alias {
+ font-weight: bold;
+ color: #111827;
+ font-size: 1.05em;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ flex: 1;
+ min-width: 0;
}
/* REPO OPEN */
.RepoOpen {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: center;
- box-shadow:
- 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 1px 2px rgba(0, 0, 0, 0.24);
- width: auto;
- max-height: 200px;
- margin: 20px 0px 0px 0px;
- padding: 15px;
- border-radius: 5px;
- transition: max-height 0.1s linear;
- overflow: visible;
- /* Need to display comment on hover (which is position : absolute) */
- position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ box-shadow:
+ 0 1px 3px rgba(0, 0, 0, 0.12),
+ 0 1px 2px rgba(0, 0, 0, 0.24);
+ width: auto;
+ margin: 20px 0px 0px 0px;
+ padding: 15px;
+ border-radius: 5px;
+ transition: max-height 0.1s linear;
+ overflow: visible;
+ /* Need to display comment on hover (which is position : absolute) */
+ position: relative;
+ background: #fff;
}
.openFlex {
- display: flex;
- align-items: center;
- align-self: flex-start;
- width: 100%;
+ display: block;
+ width: 100%;
+}
+
+.aliasFlex {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+}
+.indicatorsFlex {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-end;
+ width: 100%;
+ gap: 15px;
}
.tabInfo {
- width: 100%;
- overflow-wrap: break-word;
- border-collapse: collapse;
- background: #fff;
- border-radius: 10px;
- overflow: hidden;
- margin: 25px auto;
- table-layout: fixed;
+ width: 100%;
+ overflow-wrap: break-word;
+ border-collapse: collapse;
+ background: #fff;
+ border-radius: 10px;
+ overflow: hidden;
+ margin: 15px auto;
+ table-layout: fixed;
}
.tabInfo thead tr {
- height: 50px;
- background: #111827;
- color: #fff;
+ height: 50px;
+ background: #111827;
+ color: #fff;
}
.tabInfo thead th {
- font-size: 1em;
- color: #fff;
- line-height: 1.2;
- font-weight: normal;
+ font-size: 1em;
+ color: #fff;
+ line-height: 1.2;
+ font-weight: 500;
}
.tabInfo tbody tr {
- background-color: #f3f4f6;
- height: 50px;
+ background-color: #f3f4f6;
+ height: 50px;
}
.tabInfo tbody tr th {
- color: #65748b;
- font-size: 0.95rem;
- font-weight: 400;
+ color: #65748b;
+ font-size: 0.95rem;
+ font-weight: 400;
}
/*STATUS*/
-
-.statusIndicatorGreen {
- background: rgb(9, 255, 0);
- border-radius: 50%;
- margin: 10px;
- height: 15px;
- width: 15px;
- box-shadow: 0 0 0 0 rgb(9, 255, 0);
- transform: scale(1);
- animation: pulseGreen 5s infinite;
- animation-delay: 1s;
+.statusIndicatorGreen,
+.statusIndicatorRed {
+ border-radius: 50%;
+ height: 16px;
+ width: 16px;
+ flex-shrink: 0;
+ animation: pulse 5s infinite;
}
-@keyframes pulseGreen {
- 0% {
- transform: scale(0.95);
- box-shadow: 0 0 0 0 rgba(17, 255, 0, 0.7);
- }
-
- 10% {
- transform: scale(1);
- box-shadow: 0 0 0 10px rgba(17, 255, 0, 0);
- }
-
- 90% {
- transform: scale(0.95);
- box-shadow: 0 0 0 0 rgba(17, 255, 0, 0);
- }
+.statusIndicatorGreen {
+ background: #00d26a;
+ box-shadow: 0 0 0 0 rgba(0, 210, 106, 0.7);
}
.statusIndicatorRed {
- background: rgb(255, 0, 0);
- border-radius: 50%;
- margin: 10px;
- height: 15px;
- width: 15px;
-
- box-shadow: 0 0 0 0 rgb(255, 0, 0);
- transform: scale(1);
- animation: pulseRed 5s infinite;
- animation-delay: 0.5s;
+ background: #ff3d3d;
+ box-shadow: 0 0 0 0 rgba(255, 61, 61, 0.7);
+ animation-delay: 0.5s;
}
-@keyframes pulseRed {
- 0% {
- transform: scale(0.95);
- box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7);
- }
-
- 10% {
- transform: scale(1);
- box-shadow: 0 0 0 10px rgba(255, 0, 0, 0);
- }
-
- 90% {
- transform: scale(0.95);
- box-shadow: 0 0 0 0 rgba(255, 0, 0, 0);
- }
+@keyframes pulse {
+ 0% {
+ transform: scale(0.95);
+ box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.4);
+ }
+ 10% {
+ transform: scale(1);
+ box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
+ }
+ 90% {
+ transform: scale(0.95);
+ box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
+ }
}
/* Alert icon */
-
.alertIcon {
- display: flex;
- flex-direction: row;
- align-items: center;
- margin-left: 10px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
}
.appendOnlyModeIcon {
- display: flex;
- flex-direction: row;
- align-items: center;
- margin-left: 10px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
}
/* GENERAL */
.alias {
- font-weight: bold;
- color: #111827;
- font-size: 1.05em;
+ font-weight: bold;
+ color: #111827;
+ font-size: 1.05em;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.RepoOpen .alias {
+ margin-top: 5px;
}
.lastSave {
- color: #65748b;
+ color: #65748b;
}
.editButton {
- cursor: pointer;
+ cursor: pointer;
}
/* Comment */
.comment {
- display: flex;
- flex-direction: row;
- align-items: center;
- margin-left: 10px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
}
.toolTip {
- visibility: hidden;
- width: auto;
- height: auto;
- max-width: 400px;
- max-height: 250px;
- background-color: #fff;
- color: #637381;
- text-align: center;
- border-radius: 6px;
- padding: 5px 5px;
- position: absolute;
- z-index: 1;
- margin: 0px 0 0 20px;
- opacity: 1;
- transition: 0.5s opacity;
- box-shadow:
- 0 3px 6px rgba(0, 0, 0, 0.16),
- 0 3px 6px rgba(0, 0, 0, 0.23);
- overflow: auto;
+ visibility: hidden;
+ width: auto;
+ height: auto;
+ max-width: 400px;
+ max-height: 250px;
+ background-color: #fff;
+ color: #637381;
+ text-align: center;
+ border-radius: 6px;
+ padding: 5px 5px;
+ position: absolute;
+ z-index: 1;
+ margin: 0px 0 0 20px;
+ opacity: 1;
+ transition: 0.5s opacity;
+ box-shadow:
+ 0 3px 6px rgba(0, 0, 0, 0.16),
+ 0 3px 6px rgba(0, 0, 0, 0.23);
+ overflow: auto;
}
.comment:hover .toolTip,
.comment:active .toolTip {
- visibility: visible;
- opacity: 1;
+ visibility: visible;
+ opacity: 1;
}
.chevron {
- margin: auto;
+ margin: auto;
}
.chevron :focus,
.chevron :hover {
- cursor: pointer;
- filter: invert(27%) sepia(82%) saturate(2209%) hue-rotate(240deg)
- brightness(99%) contrast(105%);
+ cursor: pointer;
+ filter: invert(27%) sepia(82%) saturate(2209%) hue-rotate(240deg) brightness(99%) contrast(105%);
}
/* MOBILE */
@media all and (max-width: 1000px) {
- .tabInfo {
- display: none;
- }
- .toolTip {
- display: none;
- }
- .comment {
- display: none;
- }
- .lastSave {
- display: none;
- }
- .closeFlex {
- margin: auto;
- }
- .openFlex {
- margin: auto;
- width: auto;
- }
+ .openFlex,
+ .tabInfo,
+ .toolTip,
+ .comment,
+ .chevron {
+ display: none !important;
+ }
+
+ .RepoOpen,
+ .RepoClose {
+ display: flex !important;
+ flex-direction: row !important;
+ align-items: center !important;
+ justify-content: space-between !important;
+ max-height: 65px !important;
+ padding: 15px !important;
+ margin: 20px 0 0 0 !important;
+ }
+
+ .closeFlex {
+ display: flex !important;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 0 !important;
+ margin: 0 !important;
+ }
+
+ .alias {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .leftGroup {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ min-width: 0;
+ gap: 10px;
+ }
+
+ .rightGroup {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-shrink: 0;
+ }
+
+ .lastSave {
+ display: block !important;
+ color: #65748b;
+ white-space: nowrap;
+ font-size: 0.85em;
+ flex-shrink: 0;
+ margin-left: 10px;
+ }
+
+ .appendOnlyModeIcon,
+ .alertIcon {
+ display: flex;
+ align-items: center;
+ }
}
diff --git a/Components/Repo/Repo.tsx b/Components/Repo/Repo.tsx
new file mode 100644
index 0000000..a659670
--- /dev/null
+++ b/Components/Repo/Repo.tsx
@@ -0,0 +1,261 @@
+import { useState, useMemo } from 'react';
+import classes from './Repo.module.css';
+import {
+ IconSettings,
+ IconInfoCircle,
+ IconChevronDown,
+ IconChevronUp,
+ IconBellOff,
+ IconLockPlus,
+} from '@tabler/icons-react';
+import StorageBar from '../UI/StorageBar/StorageBar';
+import QuickCommands from './QuickCommands/QuickCommands';
+import { Repository, WizardEnvType, Optional } from '~/types';
+import { fromUnixTime, formatDistanceStrict } from 'date-fns';
+import useMedia from 'use-media';
+
+type RepoProps = Omit & {
+ repoManageEditHandler: () => void;
+ wizardEnv: Optional;
+};
+
+export default function Repo(props: RepoProps) {
+ const isMobile = useMedia({ maxWidth: 1000 });
+
+ const currentDate = useMemo(() => new Date(), []);
+
+ //Load displayDetails from LocalStorage
+ const displayDetailsFromLS = (): boolean => {
+ const key = `displayDetailsRepo${props.id}`;
+
+ try {
+ const storedValue = localStorage.getItem(key);
+
+ if (storedValue === null) {
+ const defaultValue = true;
+ localStorage.setItem(key, JSON.stringify(defaultValue));
+ return defaultValue;
+ }
+
+ const parsedValue = JSON.parse(storedValue);
+ if (typeof parsedValue === 'boolean') {
+ return parsedValue;
+ }
+
+ localStorage.removeItem(key);
+ return true;
+ } catch (error) {
+ localStorage.removeItem(key);
+ return true;
+ }
+ };
+
+ //States
+ const [displayDetails, setDisplayDetails] = useState(displayDetailsFromLS);
+
+ //BUTTON : Display or not repo details for ONE repo
+ const displayDetailsForOneHandler = (boolean: boolean) => {
+ //Update localStorage
+ localStorage.setItem('displayDetailsRepo' + props.id, JSON.stringify(boolean));
+ setDisplayDetails(boolean);
+ };
+
+ //Status indicator
+ const statusIndicator = () => {
+ return props.status ? classes.statusIndicatorGreen : classes.statusIndicatorRed;
+ };
+
+ //Alert indicator
+ const alertIndicator = () => {
+ if (props.alert === 0) {
+ return (
+
+
+
+ );
+ }
+ };
+
+ const appendOnlyModeIndicator = () => {
+ if (props.appendOnlyMode) {
+ return (
+
+
+
+ );
+ }
+ };
+
+ const mobileView = () => {
+ return (
+ <>
+
+
+
+ {appendOnlyModeIndicator()}
+ {alertIndicator()}
+ {props.comment && (
+
+ )}
+
+
+
+ {props.lastSave === 0
+ ? '-'
+ : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, {
+ addSuffix: true,
+ })}
+
+
+
+
+ >
+ );
+ };
+
+ if (isMobile) {
+ return mobileView();
+ } else {
+ return (
+ <>
+ {displayDetails ? (
+ <>
+
+
+
+ {props.comment && (
+
+ )}
+ {appendOnlyModeIndicator()}
+ {alertIndicator()}
+
+
+
+
+
+
+
+ Repository
+ Storage Size
+ Storage Used
+ Last change
+ Edit
+
+
+
+
+ {props.repositoryName}
+ {props.storageSize} GB
+
+
+
+
+
+ {props.lastSave === 0
+ ? '-'
+ : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, {
+ addSuffix: true,
+ })}
+
+
+
+
+ props.repoManageEditHandler()}
+ />
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+ {appendOnlyModeIndicator()}
+ {alertIndicator()}
+ {props.comment && (
+
+ )}
+
+
+
+ {props.lastSave === 0
+ ? '-'
+ : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, {
+ addSuffix: true,
+ })}
+
+
+
+
+ >
+ )}
+ {displayDetails ? (
+
+ {
+ displayDetailsForOneHandler(false);
+ }}
+ />
+
+ ) : (
+
+ {
+ displayDetailsForOneHandler(true);
+ }}
+ />
+
+ )}
+ >
+ );
+ }
+}
diff --git a/Components/UI/CopyButton/CopyButton.js b/Components/UI/CopyButton/CopyButton.js
deleted file mode 100644
index 0079d35..0000000
--- a/Components/UI/CopyButton/CopyButton.js
+++ /dev/null
@@ -1,39 +0,0 @@
-//Lib
-import classes from './CopyButton.module.css';
-import { useState } from 'react';
-import { IconCopy } from '@tabler/icons-react';
-
-export default function CopyButton(props) {
- //State
- const [isCopied, setIsCopied] = useState(false);
-
- //Function
- const handleCopy = async (data) => {
- navigator.clipboard
- .writeText(data)
- .then(() => {
- // If successful, update the isCopied state value
- setIsCopied(true);
- setTimeout(() => {
- setIsCopied(false);
- }, 1500);
- })
- .catch((err) => {
- console.log(err);
- });
- };
-
- return (
- <>
- handleCopy(props.dataToCopy)}
- >
-
-
- {isCopied ? (
- Copied !
- ) : null}
- >
- );
-}
diff --git a/Components/UI/CopyButton/CopyButton.module.css b/Components/UI/CopyButton/CopyButton.module.css
index ab59788..9e8a4ea 100644
--- a/Components/UI/CopyButton/CopyButton.module.css
+++ b/Components/UI/CopyButton/CopyButton.module.css
@@ -1,26 +1,35 @@
.copyButton {
- visibility: visible;
- opacity: 1;
- border: none;
- background-color: transparent;
- cursor: pointer;
+ visibility: visible;
+ opacity: 1;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+}
+
+.copyButton span {
+ font-size: 0.95rem;
+ color: #6d4aff;
+ margin-right: 5px;
+ user-select: text;
}
.copyValid {
- font-size: 0.95rem;
- color: #6d4aff;
- animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
+ font-size: 0.95rem;
+ color: #6d4aff;
+ animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
@keyframes scale-in-center {
- 0% {
- -webkit-transform: scale(0);
- transform: scale(0);
- opacity: 1;
- }
- 100% {
- -webkit-transform: scale(1);
- transform: scale(1);
- opacity: 1;
- }
+ 0% {
+ -webkit-transform: scale(0);
+ transform: scale(0);
+ opacity: 1;
+ }
+ 100% {
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ opacity: 1;
+ }
}
diff --git a/Components/UI/CopyButton/CopyButton.tsx b/Components/UI/CopyButton/CopyButton.tsx
new file mode 100644
index 0000000..1258f39
--- /dev/null
+++ b/Components/UI/CopyButton/CopyButton.tsx
@@ -0,0 +1,46 @@
+import classes from './CopyButton.module.css';
+import { useState, ReactNode } from 'react';
+import { IconChecks, IconCopy } from '@tabler/icons-react';
+
+type CopyButtonProps = {
+ dataToCopy: string;
+ children?: ReactNode;
+ displayIconConfirmation?: boolean;
+ size?: number;
+ stroke?: number;
+};
+
+export default function CopyButton(props: CopyButtonProps) {
+ const [isCopied, setIsCopied] = useState(false);
+
+ const handleCopy = async (data: string) => {
+ navigator.clipboard
+ .writeText(data)
+ .then(() => {
+ // If successful, update the isCopied state value
+ setIsCopied(true);
+ setTimeout(() => {
+ setIsCopied(false);
+ }, 1500);
+ })
+ .catch((err) => {
+ console.log(err);
+ });
+ };
+
+ return (
+ <>
+ handleCopy(props.dataToCopy)}>
+ {props.children}
+ {isCopied && props.displayIconConfirmation ? (
+
+ ) : (
+
+ )}
+
+ {isCopied
+ ? !props.displayIconConfirmation && Copied !
+ : null}
+ >
+ );
+}
diff --git a/Components/UI/Error/Error.js b/Components/UI/Error/Error.js
deleted file mode 100644
index 2a3fe1f..0000000
--- a/Components/UI/Error/Error.js
+++ /dev/null
@@ -1,6 +0,0 @@
-//Lib
-import classes from './Error.module.css';
-
-export default function Error(props) {
- return {props.message}
;
-}
diff --git a/Components/UI/Error/Error.module.css b/Components/UI/Error/Error.module.css
index 19b75f6..9947cb2 100644
--- a/Components/UI/Error/Error.module.css
+++ b/Components/UI/Error/Error.module.css
@@ -1,18 +1,18 @@
.errorMessage {
- margin: 15px 0px;
- background-color: red;
- color: white;
- padding: 15px;
- border-radius: 5px;
- animation: myAnim 1s ease 0s 1 normal forwards;
+ margin: 15px 0px;
+ background-color: red;
+ color: white;
+ padding: 15px;
+ border-radius: 5px;
+ animation: myAnim 1s ease 0s 1 normal forwards;
}
@keyframes myAnim {
- 0% {
- opacity: 0;
- }
+ 0% {
+ opacity: 0;
+ }
- 100% {
- opacity: 1;
- }
+ 100% {
+ opacity: 1;
+ }
}
diff --git a/Components/UI/Error/Error.tsx b/Components/UI/Error/Error.tsx
new file mode 100644
index 0000000..d0c4da8
--- /dev/null
+++ b/Components/UI/Error/Error.tsx
@@ -0,0 +1,9 @@
+import classes from './Error.module.css';
+
+type ErrorProps = {
+ message: string;
+};
+
+export default function Error(props: ErrorProps) {
+ return {props.message}
;
+}
diff --git a/Components/UI/Info/Info.js b/Components/UI/Info/Info.js
deleted file mode 100644
index e06c0bd..0000000
--- a/Components/UI/Info/Info.js
+++ /dev/null
@@ -1,6 +0,0 @@
-//Lib
-import classes from './Info.module.css';
-
-export default function Info(props) {
- return {props.message}
;
-}
diff --git a/Components/UI/Info/Info.module.css b/Components/UI/Info/Info.module.css
index 0bb624d..d6df06d 100644
--- a/Components/UI/Info/Info.module.css
+++ b/Components/UI/Info/Info.module.css
@@ -1,18 +1,18 @@
.infoMessage {
- margin: 15px 0px;
- background-color: rgb(17, 147, 0);
- color: white;
- padding: 15px;
- border-radius: 5px;
- animation: myAnim 1s ease 0s 1 normal forwards;
+ margin: 15px 0px;
+ background-color: rgb(17, 147, 0);
+ color: white;
+ padding: 15px;
+ border-radius: 5px;
+ animation: myAnim 1s ease 0s 1 normal forwards;
}
@keyframes myAnim {
- 0% {
- opacity: 0;
- }
+ 0% {
+ opacity: 0;
+ }
- 100% {
- opacity: 1;
- }
+ 100% {
+ opacity: 1;
+ }
}
diff --git a/Components/UI/Info/Info.tsx b/Components/UI/Info/Info.tsx
new file mode 100644
index 0000000..8e2f729
--- /dev/null
+++ b/Components/UI/Info/Info.tsx
@@ -0,0 +1,17 @@
+import { ReactNode } from 'react';
+import classes from './Info.module.css';
+
+type InfoProps = {
+ message: string;
+ color?: string;
+ children?: ReactNode;
+};
+
+export default function Info(props: InfoProps) {
+ return (
+
+ {props.message}
+ {props.children}
+
+ );
+}
diff --git a/Components/UI/Layout/Footer/Footer.js b/Components/UI/Layout/Footer/Footer.js
deleted file mode 100644
index 10e2566..0000000
--- a/Components/UI/Layout/Footer/Footer.js
+++ /dev/null
@@ -1,24 +0,0 @@
-//Lib
-import classes from './Footer.module.css';
-import packageInfo from '../../../../package.json';
-
-function Footer() {
- return (
-
- );
-}
-
-export default Footer;
diff --git a/Components/UI/Layout/Footer/Footer.module.css b/Components/UI/Layout/Footer/Footer.module.css
index c966167..e19fc52 100644
--- a/Components/UI/Layout/Footer/Footer.module.css
+++ b/Components/UI/Layout/Footer/Footer.module.css
@@ -1,26 +1,25 @@
.footer {
- color: #494b7a;
- text-align: center;
- position: absolute;
- bottom: 0;
- width: 100%;
- height: 50px;
+ color: #494b7a;
+ text-align: center;
+ position: absolute;
+ bottom: 0;
+ width: 100%;
}
.footer p {
- padding-left: 70px;
+ padding-left: 70px;
}
a.site {
- color: #6d4aff;
- text-decoration: none;
+ color: #6d4aff;
+ text-decoration: none;
}
@media all and (max-width: 1000px) {
- .footer {
- width: 100%;
- }
- .footer p {
- padding-left: 0;
- }
+ .footer {
+ width: 100%;
+ }
+ .footer p {
+ padding-left: 0;
+ }
}
diff --git a/Components/UI/Layout/Footer/Footer.tsx b/Components/UI/Layout/Footer/Footer.tsx
new file mode 100644
index 0000000..58cd034
--- /dev/null
+++ b/Components/UI/Layout/Footer/Footer.tsx
@@ -0,0 +1,23 @@
+import classes from './Footer.module.css';
+import packageInfo from '~/package.json';
+
+function Footer() {
+ return (
+
+ );
+}
+
+export default Footer;
diff --git a/Components/UI/Layout/Header/Header.js b/Components/UI/Layout/Header/Header.js
deleted file mode 100644
index 9bb2f64..0000000
--- a/Components/UI/Layout/Header/Header.js
+++ /dev/null
@@ -1,21 +0,0 @@
-//Lib
-import classes from './Header.module.css';
-
-//Components
-import Nav from './Nav/Nav';
-
-function Header() {
- return (
-
-
-
BorgWarehouse
-
-
-
-
-
-
- );
-}
-
-export default Header;
diff --git a/Components/UI/Layout/Header/Header.module.css b/Components/UI/Layout/Header/Header.module.css
index c213172..2156d01 100644
--- a/Components/UI/Layout/Header/Header.module.css
+++ b/Components/UI/Layout/Header/Header.module.css
@@ -1,31 +1,31 @@
.Header {
- width: 100%;
- background: #111827;
- box-shadow:
- 0 3px 6px rgba(0, 0, 0, 0.16),
- 0 3px 6px rgba(0, 0, 0, 0.23);
- height: 50px;
- color: white;
- display: flex;
- align-items: center;
- position: static;
- top: 0;
- z-index: 1000;
+ width: 100%;
+ background: #111827;
+ box-shadow:
+ 0 3px 6px rgba(0, 0, 0, 0.16),
+ 0 3px 6px rgba(0, 0, 0, 0.23);
+ height: 50px;
+ color: white;
+ display: flex;
+ align-items: center;
+ position: static;
+ top: 0;
+ z-index: 1000;
}
.flex {
- display: flex;
- align-items: center;
- justify-content: space-between;
- width: 100%;
- max-width: 1500px;
- margin: auto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ max-width: 1500px;
+ margin: auto;
}
.logo {
- font-size: 1.5em;
- font-weight: bold;
- color: #6d4aff;
- text-shadow: #6d4aff 0px 0px 18px;
- margin-left: 20px;
+ font-size: 1.5em;
+ font-weight: bold;
+ color: #6d4aff;
+ text-shadow: #6d4aff 0px 0px 18px;
+ margin-left: 70px;
}
diff --git a/Components/UI/Layout/Header/Header.tsx b/Components/UI/Layout/Header/Header.tsx
new file mode 100644
index 0000000..1067f6e
--- /dev/null
+++ b/Components/UI/Layout/Header/Header.tsx
@@ -0,0 +1,28 @@
+import Image from 'next/image';
+import classes from './Header.module.css';
+import Nav from './Nav/Nav';
+
+function Header() {
+ return (
+
+ );
+}
+
+export default Header;
diff --git a/Components/UI/Layout/Header/Nav/Nav.js b/Components/UI/Layout/Header/Nav/Nav.js
deleted file mode 100644
index 24d035b..0000000
--- a/Components/UI/Layout/Header/Nav/Nav.js
+++ /dev/null
@@ -1,57 +0,0 @@
-//Lib
-import classes from './Nav.module.css';
-import { IconUser, IconLogout } from '@tabler/icons-react';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import { useSession, signOut } from 'next-auth/react';
-
-export default function Nav() {
- ////Var
- //Get the current route to light the right Item
- const router = useRouter();
- const currentRoute = router.pathname;
- const { status, data } = useSession();
-
- //Function
- const onLogoutClickedHandler = async () => {
- //This bug is open : https://github.com/nextauthjs/next-auth/issues/1542
- //I put redirect to false and redirect with router.
- //The result on logout click is an ugly piece of page for a few milliseconds before returning to the login page.
- //It's ugly if you are perfectionist but functional and invisible for most of users while waiting for a next-auth fix.
- await signOut({ redirect: false });
- router.replace('/login');
- };
-
- return (
-
-
-
-
-
-
-
-
- {status === 'authenticated' && data.user.name}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/Components/UI/Layout/Header/Nav/Nav.module.css b/Components/UI/Layout/Header/Nav/Nav.module.css
index 5b8c396..8032f69 100644
--- a/Components/UI/Layout/Header/Nav/Nav.module.css
+++ b/Components/UI/Layout/Header/Nav/Nav.module.css
@@ -1,54 +1,54 @@
.Nav {
- list-style-type: none;
- margin: 0px 15px 0px 0px;
- padding: 0;
- display: flex;
+ list-style-type: none;
+ margin: 0px 15px 0px 0px;
+ padding: 0;
+ display: flex;
}
.user {
- display: flex;
- align-items: center;
+ display: flex;
+ align-items: center;
}
.username::first-letter {
- text-transform: capitalize;
+ text-transform: capitalize;
}
.account {
- background: none;
- border: none;
- cursor: pointer;
- color: #494b7a;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: #494b7a;
}
.account a {
- color: #494b7a;
- text-decoration: none;
+ color: #494b7a;
+ text-decoration: none;
}
.account :focus,
.account .active,
.account :hover {
- color: #6d4aff;
- text-shadow: #6d4aff 0px 0px 18px;
+ color: #6d4aff;
+ text-shadow: #6d4aff 0px 0px 18px;
}
.logout {
- background: none;
- border: none;
- cursor: pointer;
- color: #494b7a;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: #494b7a;
}
.logout :focus,
.logout .active,
.logout :hover {
- color: #6d4aff;
- text-shadow: #6d4aff 0px 0px 18px;
+ color: #6d4aff;
+ text-shadow: #6d4aff 0px 0px 18px;
}
@media all and (max-width: 1000px) {
- .account {
- display: none;
- }
+ .account {
+ display: none;
+ }
}
diff --git a/Components/UI/Layout/Header/Nav/Nav.tsx b/Components/UI/Layout/Header/Nav/Nav.tsx
new file mode 100644
index 0000000..1ccba41
--- /dev/null
+++ b/Components/UI/Layout/Header/Nav/Nav.tsx
@@ -0,0 +1,43 @@
+import classes from './Nav.module.css';
+import { IconUser, IconLogout } from '@tabler/icons-react';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { useSession, signOut } from 'next-auth/react';
+
+export default function Nav() {
+ const router = useRouter();
+ const currentRoute = router.pathname;
+ const { status, data } = useSession();
+
+ const onLogoutClickedHandler = async () => {
+ //This bug is open : https://github.com/nextauthjs/next-auth/issues/1542
+ //I put redirect to false and redirect with router.
+ //The result on logout click is an ugly piece of page for a few milliseconds before returning to the login page.
+ //It's ugly if you are perfectionist but functional and invisible for most of users while waiting for a next-auth fix.
+ await signOut({ redirect: false });
+ router.replace('/login');
+ };
+
+ return (
+
+
+
+
+
+
+
+
{status === 'authenticated' && data.user?.name}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/Components/UI/Layout/Layout.js b/Components/UI/Layout/Layout.js
deleted file mode 100644
index 96fcedd..0000000
--- a/Components/UI/Layout/Layout.js
+++ /dev/null
@@ -1,30 +0,0 @@
-//Lib
-import Footer from './Footer/Footer';
-import Header from './Header/Header';
-import NavSide from './NavSide/NavSide';
-import classes from './Layout.module.css';
-import { useSession } from 'next-auth/react';
-
-function Layout(props) {
- //Var
- const { status } = useSession();
-
- if (status === 'authenticated') {
- return (
- <>
-
-
- {props.children}
-
- >
- );
- } else if (status === 'unauthenticated') {
- return (
- <>
- {props.children}
- >
- );
- }
-}
-
-export default Layout;
diff --git a/Components/UI/Layout/Layout.module.css b/Components/UI/Layout/Layout.module.css
index c49a84f..1d428e9 100644
--- a/Components/UI/Layout/Layout.module.css
+++ b/Components/UI/Layout/Layout.module.css
@@ -1,23 +1,23 @@
.mainWrapper {
- margin: auto;
- max-width: 1400px;
- height: calc(100vh - 100px);
- display: flex;
- justify-content: center;
- align-items: flex-start;
- overflow: auto;
- /* to prevent main content under navside on little screen */
- padding-left: 90px;
- /* Disable scrollbar */
- scrollbar-width: none;
+ margin: auto;
+ max-width: 1400px;
+ height: calc(100vh - 100px);
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ overflow: auto;
+ /* to prevent main content under navside on little screen */
+ padding-left: 90px;
+ /* Disable scrollbar */
+ scrollbar-width: none;
}
.login {
- background-color: #111827;
+ background-color: #111827;
}
@media all and (max-width: 1000px) {
- .mainWrapper {
- padding-left: 0px;
- }
+ .mainWrapper {
+ padding-left: 0px;
+ }
}
diff --git a/Components/UI/Layout/Layout.tsx b/Components/UI/Layout/Layout.tsx
new file mode 100644
index 0000000..b9444e4
--- /dev/null
+++ b/Components/UI/Layout/Layout.tsx
@@ -0,0 +1,32 @@
+import Footer from './Footer/Footer';
+import Header from './Header/Header';
+import NavSide from './NavSide/NavSide';
+import classes from './Layout.module.css';
+import { useSession } from 'next-auth/react';
+
+type LayoutProps = {
+ children: React.ReactNode;
+};
+
+function Layout(props: LayoutProps) {
+ const { status } = useSession();
+
+ if (status === 'authenticated') {
+ return (
+ <>
+
+
+ {props.children}
+
+ >
+ );
+ } else if (status === 'unauthenticated') {
+ return (
+ <>
+ {props.children}
+ >
+ );
+ }
+}
+
+export default Layout;
diff --git a/Components/UI/Layout/NavSide/NavSide.js b/Components/UI/Layout/NavSide/NavSide.js
deleted file mode 100644
index 921ef1b..0000000
--- a/Components/UI/Layout/NavSide/NavSide.js
+++ /dev/null
@@ -1,56 +0,0 @@
-//Lib
-import classes from './NavSide.module.css';
-import {
- IconServer,
- IconSettingsAutomation,
- IconActivityHeartbeat,
-} from '@tabler/icons-react';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-
-//Composants
-
-export default function NavSide() {
- ////Var
- //Get the current route to light the right Item
- const router = useRouter();
- const currentRoute = router.pathname;
-
- return (
-
-
-
-
-
- Repositories
-
-
-
-
-
- Setup Wizard
-
-
-
-
-
- Monitoring
-
-
- );
-}
diff --git a/Components/UI/Layout/NavSide/NavSide.module.css b/Components/UI/Layout/NavSide/NavSide.module.css
index 819f36c..8771b85 100644
--- a/Components/UI/Layout/NavSide/NavSide.module.css
+++ b/Components/UI/Layout/NavSide/NavSide.module.css
@@ -1,74 +1,74 @@
/* NAVSIDE */
.NavSide {
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- position: absolute;
- top: 50px;
- left: 0;
- bottom: 0;
- text-align: center;
- /* border-right: 2px solid #e5e7eb; */
- height: calc(100% - 50px);
- width: 70px;
- list-style-type: none;
- /* background: #1b1340; */
- background: #111827;
- /* box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); */
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ position: absolute;
+ top: 50px;
+ left: 0;
+ bottom: 0;
+ text-align: center;
+ /* border-right: 2px solid #e5e7eb; */
+ height: calc(100% - 50px);
+ width: 70px;
+ list-style-type: none;
+ /* background: #1b1340; */
+ background: #111827;
+ /* box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); */
}
ul.NavSide {
- padding-top: 10px;
- margin: 0;
- padding-inline-start: 0px;
+ padding-top: 10px;
+ margin: 0;
+ padding-inline-start: 0px;
}
/* NAV SIDE ITEMS */
.NavSideItem {
- margin: 0px 0px 30px 0px;
+ margin: 0px 0px 30px 0px;
}
.NavSideItem a {
- text-decoration: none;
- color: #494b7a;
- font-weight: bold;
+ text-decoration: none;
+ color: #494b7a;
+ font-weight: bold;
}
.NavSideItem a:hover,
.NavSideItem a:focus,
.NavSideItem a.active {
- color: #6d4aff;
- font-weight: bold;
- /* border-bottom: 2px solid #6d4aff; */
- /* padding-bottom: 15px; */
- text-shadow: #6d4aff 0px 0px 18px;
+ color: #6d4aff;
+ font-weight: bold;
+ /* border-bottom: 2px solid #6d4aff; */
+ /* padding-bottom: 15px; */
+ text-shadow: #6d4aff 0px 0px 18px;
}
.tooltip {
- visibility: hidden;
- width: 120px;
- /* background-color: #1b1340; */
- background-color: #111827;
- color: #d1d5db;
- text-align: center;
- border-radius: 6px;
- padding: 5px 0;
- position: absolute;
- z-index: 1;
- margin: 5px 0 0 20px;
- opacity: 0;
- transition: 0.5s opacity;
- box-shadow: 0 1px 8px rgba(0, 0, 0, 0.251);
+ visibility: hidden;
+ width: 120px;
+ /* background-color: #1b1340; */
+ background-color: #111827;
+ color: #d1d5db;
+ text-align: center;
+ border-radius: 6px;
+ padding: 5px 0;
+ position: absolute;
+ z-index: 1;
+ margin: 5px 0 0 20px;
+ opacity: 0;
+ transition: 0.5s opacity;
+ box-shadow: 0 1px 8px rgba(0, 0, 0, 0.251);
}
.NavSideItem:hover .tooltip {
- visibility: visible;
- opacity: 1;
+ visibility: visible;
+ opacity: 1;
}
@media all and (max-width: 1000px) {
- .NavSide {
- display: none;
- }
+ .NavSide {
+ display: none;
+ }
}
diff --git a/Components/UI/Layout/NavSide/NavSide.tsx b/Components/UI/Layout/NavSide/NavSide.tsx
new file mode 100644
index 0000000..48290ae
--- /dev/null
+++ b/Components/UI/Layout/NavSide/NavSide.tsx
@@ -0,0 +1,38 @@
+import classes from './NavSide.module.css';
+import { IconServer, IconSettingsAutomation, IconActivityHeartbeat } from '@tabler/icons-react';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+
+export default function NavSide() {
+ const router = useRouter();
+ const currentRoute = router.pathname;
+
+ return (
+
+
+
+
+
+ Repositories
+
+
+
+
+
+ Setup Wizard
+
+
+
+
+
+ Monitoring
+
+
+ );
+}
diff --git a/Components/UI/ShimmerRepoList/ShimmerRepoList.js b/Components/UI/ShimmerRepoList/ShimmerRepoList.js
deleted file mode 100644
index 537879d..0000000
--- a/Components/UI/ShimmerRepoList/ShimmerRepoList.js
+++ /dev/null
@@ -1,19 +0,0 @@
-//Lib
-import classes from './ShimmerRepoList.module.css';
-
-export default function ShimmerRepoList() {
- return (
-
- );
-}
diff --git a/Components/UI/ShimmerRepoList/ShimmerRepoList.module.css b/Components/UI/ShimmerRepoList/ShimmerRepoList.module.css
index e0045c1..4d386f9 100644
--- a/Components/UI/ShimmerRepoList/ShimmerRepoList.module.css
+++ b/Components/UI/ShimmerRepoList/ShimmerRepoList.module.css
@@ -1,50 +1,50 @@
.container {
- display: flex;
- width: 90%;
- justify-content: center;
- flex-direction: column;
+ display: flex;
+ width: 90%;
+ justify-content: center;
+ flex-direction: column;
}
.loadingButtonContainer {
- display: flex;
- flex-direction: column;
- margin: 20px auto;
- width: 90%;
- justify-content: center;
+ display: flex;
+ flex-direction: column;
+ margin: 20px auto;
+ width: 90%;
+ justify-content: center;
}
.buttonIsLoading {
- height: 62px;
- width: 211px;
- margin: auto;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
- background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%);
- border-radius: 5px;
- background-size: 200% 100%;
- animation: 1.5s shine linear infinite;
+ height: 62px;
+ width: 211px;
+ margin: auto;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
+ background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%);
+ border-radius: 5px;
+ background-size: 200% 100%;
+ animation: 1.5s shine linear infinite;
}
.loadingRepoContainer {
- display: flex;
- justify-content: center;
- flex-direction: column;
- width: 100%;
- margin: auto;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ width: 100%;
+ margin: auto;
}
.repoIsLoading {
- width: 100%;
- height: 65px;
- margin: 20px auto;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
- background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%);
- border-radius: 5px;
- background-size: 200% 100%;
- animation: 1.5s shine linear infinite;
+ width: 100%;
+ height: 65px;
+ margin: 20px auto;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
+ background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%);
+ border-radius: 5px;
+ background-size: 200% 100%;
+ animation: 1.5s shine linear infinite;
}
@keyframes shine {
- to {
- background-position-x: -200%;
- }
+ to {
+ background-position-x: -200%;
+ }
}
diff --git a/Components/UI/ShimmerRepoList/ShimmerRepoList.tsx b/Components/UI/ShimmerRepoList/ShimmerRepoList.tsx
new file mode 100644
index 0000000..f23ef90
--- /dev/null
+++ b/Components/UI/ShimmerRepoList/ShimmerRepoList.tsx
@@ -0,0 +1,22 @@
+import classes from './ShimmerRepoList.module.css';
+
+const LOADING_REPO_COUNT = 5;
+
+function ShimmerRepoItem() {
+ return
;
+}
+
+export default function ShimmerRepoList() {
+ return (
+
+
+
+ {Array.from({ length: LOADING_REPO_COUNT }, (_, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/Components/UI/StorageBar/StorageBar.js b/Components/UI/StorageBar/StorageBar.js
deleted file mode 100644
index d94c482..0000000
--- a/Components/UI/StorageBar/StorageBar.js
+++ /dev/null
@@ -1,32 +0,0 @@
-//Lib
-import classes from './StorageBar.module.css';
-
-export default function StorageBar(props) {
- //Var
- //storageUsed is in octet, storageSize is in GB. Round to 1 decimal for %.
- const storageUsedPercent = (
- ((props.storageUsed / 1000000) * 100) /
- props.storageSize
- ).toFixed(1);
-
- return (
-
-
-
-
- {storageUsedPercent}% (
- {(props.storageUsed / 1000000).toFixed(1)} GB /{' '}
- {props.storageSize} GB)
-
-
-
- );
-}
diff --git a/Components/UI/StorageBar/StorageBar.module.css b/Components/UI/StorageBar/StorageBar.module.css
index 77296e2..11df1d9 100644
--- a/Components/UI/StorageBar/StorageBar.module.css
+++ b/Components/UI/StorageBar/StorageBar.module.css
@@ -1,36 +1,36 @@
.barContainer {
- margin: auto;
+ margin: auto;
}
.barBackground {
- background-color: #704dff5e;
- border-radius: 3px;
- height: 19px;
- width: 100%;
- position: relative;
+ background-color: #704dff5e;
+ border-radius: 3px;
+ height: 19px;
+ width: 100%;
+ position: relative;
}
.progressionStyle {
- background-color: #704dff;
- border-radius: 3px;
- height: 19px;
- width: 100%;
+ background-color: #704dff;
+ border-radius: 3px;
+ height: 19px;
+ width: 100%;
}
.tooltip {
- visibility: hidden;
- width: auto;
- height: auto;
- color: #fff;
- text-align: center;
- opacity: 0;
- transition: 0.5s opacity;
- position: absolute;
- left: calc(30%);
- top: 0px;
+ visibility: hidden;
+ width: auto;
+ height: auto;
+ color: #fff;
+ text-align: center;
+ opacity: 0;
+ transition: 0.5s opacity;
+ position: absolute;
+ left: calc(30%);
+ top: 0px;
}
.barBackground:hover .tooltip {
- visibility: visible;
- opacity: 1;
+ visibility: visible;
+ opacity: 1;
}
diff --git a/Components/UI/StorageBar/StorageBar.tsx b/Components/UI/StorageBar/StorageBar.tsx
new file mode 100644
index 0000000..e34b6ee
--- /dev/null
+++ b/Components/UI/StorageBar/StorageBar.tsx
@@ -0,0 +1,33 @@
+import classes from './StorageBar.module.css';
+
+type StorageBarProps = {
+ storageUsed: number;
+ storageSize: number;
+};
+
+export default function StorageBar(props: StorageBarProps) {
+ //storageUsed is in kB, storageSize is in GB. Round to 1 decimal for %.
+ const storageUsedPercent = (((props.storageUsed / 1024 ** 2) * 100) / props.storageSize).toFixed(
+ 1
+ );
+
+ return (
+
+
+
+
+ {storageUsedPercent}% ({(props.storageUsed / 1024 ** 2).toFixed(1)} GB /{' '}
+ {props.storageSize} GB)
+
+
+
+ );
+}
diff --git a/Components/UI/Switch/Switch.js b/Components/UI/Switch/Switch.js
deleted file mode 100644
index caf75d9..0000000
--- a/Components/UI/Switch/Switch.js
+++ /dev/null
@@ -1,26 +0,0 @@
-//Lib
-import classes from './Switch.module.css';
-
-export default function Switch(props) {
- return (
- <>
-
-
-
- props.onChange(e.target.checked)}
- />
-
- {props.switchName}
-
-
-
- {props.switchDescription}
-
-
- >
- );
-}
diff --git a/Components/UI/Switch/Switch.module.css b/Components/UI/Switch/Switch.module.css
index cb50cc3..90a0166 100644
--- a/Components/UI/Switch/Switch.module.css
+++ b/Components/UI/Switch/Switch.module.css
@@ -1,157 +1,84 @@
+/* Wrapper styles */
.switchWrapper {
- display: flex;
- flex-direction: column;
- margin-bottom: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 20px;
}
+/* Switch container */
.switch {
- display: flex;
+ display: flex;
+ align-items: center;
+ gap: 10px;
}
-.switchDescription {
- display: flex;
- margin: 8px 0px 0px 0px;
- color: #6c737f;
- font-size: 0.875rem;
-}
-
-.pureMaterialSwitch {
- z-index: 0;
- position: relative;
- display: inline-block;
- color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.87);
- font-family: var(
- --pure-material-font,
- 'Roboto',
- 'Segoe UI',
- BlinkMacSystemFont,
- system-ui,
- -apple-system
- );
- font-size: 16px;
- line-height: 1.5;
+/* Label */
+.switchLabel {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ position: relative;
+ user-select: none;
}
/* Input */
-.pureMaterialSwitch > input {
- appearance: none;
- -moz-appearance: none;
- -webkit-appearance: none;
- z-index: -1;
- position: absolute;
- right: 6px;
- top: -8px;
- display: block;
- margin: 0;
- border-radius: 50%;
- width: 40px;
- height: 40px;
- background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
- outline: none;
- opacity: 0;
- transform: scale(1);
- pointer-events: none;
- transition:
- opacity 0.3s 0.1s,
- transform 0.2s 0.1s;
+.switchLabel input {
+ display: none;
}
-/* Span */
-.pureMaterialSwitch > span {
- display: inline-block;
- width: 100%;
- cursor: pointer;
- font-size: 1rem;
- font-weight: 500;
- color: #494b7a;
+/* Slider */
+.switchSlider {
+ position: relative;
+ width: 40px;
+ height: 20px;
+ background: #ccc;
+ border-radius: 12px;
+ transition: #ccc 0.3s ease;
}
-/* Track */
-.pureMaterialSwitch > span::before {
- content: '';
- float: right;
- display: inline-block;
- margin: 5px 0 5px 30px;
- border-radius: 7px;
- width: 36px;
- height: 14px;
- background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
- vertical-align: top;
- transition:
- background-color 0.2s,
- opacity 0.2s;
+.switchSlider::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 16px;
+ height: 16px;
+ background: #fff;
+ border-radius: 50%;
+ transition: transform 0.3s ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
-/* Thumb */
-.pureMaterialSwitch > span::after {
- content: '';
- position: absolute;
- top: 2px;
- right: 16px;
- border-radius: 50%;
- width: 20px;
- height: 20px;
- background-color: rgb(var(--pure-material-onprimary-rgb, 255, 255, 255));
- box-shadow:
- 0 3px 1px -2px rgba(0, 0, 0, 0.2),
- 0 2px 2px 0 rgba(0, 0, 0, 0.14),
- 0 1px 5px 0 rgba(0, 0, 0, 0.12);
- transition:
- background-color 0.2s,
- transform 0.2s;
+/* Checked styles */
+.switchLabel input:checked + .switchSlider {
+ background: #704dff;
}
-/* Checked */
-.pureMaterialSwitch > input:checked {
- right: -10px;
- background-color: rgb(var(--pure-material-primary-rgb, 109, 74, 255));
+.switchLabel input:checked + .switchSlider::after {
+ transform: translateX(20px);
}
-.pureMaterialSwitch > input:checked + span::before {
- background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
+/* Disabled styles */
+.switchLabel input:disabled + .switchSlider {
+ background: #e0e0e0;
}
-.pureMaterialSwitch > input:checked + span::after {
- background-color: rgb(var(--pure-material-primary-rgb, 109, 74, 255));
- transform: translateX(16px);
+.switchLabel input:disabled + .switchSlider::after {
+ background: #bdbdbd;
}
-/* Active */
-.pureMaterialSwitch > input:active {
- opacity: 1;
- transform: scale(0);
- transition:
- transform 0s,
- opacity 0s;
+/* Switch text */
+.switchText {
+ font-size: 1rem;
+ color: #494b7a;
+ font-weight: 500;
}
-.pureMaterialSwitch > input:active + span::before {
- background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
+/* Description */
+.switchDescription {
+ font-size: 0.875rem;
+ color: #6c737f;
+ margin-top: 4px;
}
-
-.pureMaterialSwitch > input:checked:active + span::before {
- background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
-}
-
-/* Disabled */
-.pureMaterialSwitch > input:disabled + span {
- cursor: wait;
-}
-
-/* .pureMaterialSwitch > input:disabled {
- opacity: 0;
-}
-
-.pureMaterialSwitch > input:disabled + span {
- color: rgb(var(--pure-material-onsurface-rgb, 0, 0, 0));
- opacity: 0.38;
- cursor: default;
-}
-
-.pureMaterialSwitch > input:disabled + span::before {
- background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
-}
-
-.pureMaterialSwitch > input:checked:disabled + span::before {
- background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
-} */
diff --git a/Components/UI/Switch/Switch.tsx b/Components/UI/Switch/Switch.tsx
new file mode 100644
index 0000000..f32fa42
--- /dev/null
+++ b/Components/UI/Switch/Switch.tsx
@@ -0,0 +1,45 @@
+import { Optional } from '~/types';
+import classes from './Switch.module.css';
+import { useLoader } from '~/contexts/LoaderContext';
+import { useEffect } from 'react';
+
+type SwitchProps = {
+ switchName: string;
+ switchDescription: string;
+ checked: Optional;
+ disabled: boolean;
+ loading?: boolean;
+ onChange: (checked: boolean) => void;
+};
+
+export default function Switch(props: SwitchProps) {
+ const { start, stop } = useLoader();
+
+ useEffect(() => {
+ if (props.loading) {
+ start();
+ } else {
+ stop();
+ }
+ }, [props.loading, start, stop]);
+
+ return (
+
+
+
+ <>
+ props.onChange(e.target.checked)}
+ />
+
+ >
+ {props.switchName}
+
+
+
{props.switchDescription}
+
+ );
+}
diff --git a/Components/WizardSteps/WizardStep1/WizardStep1.js b/Components/WizardSteps/WizardStep1/WizardStep1.js
deleted file mode 100644
index 93b4d3c..0000000
--- a/Components/WizardSteps/WizardStep1/WizardStep1.js
+++ /dev/null
@@ -1,69 +0,0 @@
-//Lib
-import React from 'react';
-import classes from '../WizardStep1/WizardStep1.module.css';
-import { IconDeviceDesktopAnalytics, IconTerminal2 } from '@tabler/icons-react';
-
-function WizardStep1() {
- return (
-
-
-
- Command Line Interface
-
-
- We recommend using the official
BorgBackup client which
- is supported by most Linux distributions.
-
- More information about installation can be{' '}
-
- found here
-
- .
- To
automate your backup , you can also use{' '}
-
- Borgmatic
- {' '}
- which is a{' '}
-
- Debian package
-
- . On the step 4, you will find a pattern of default config.
-
-
-
-
- Graphical User Interface
-
-
-
Vorta is an opensource (GPLv3) backup client for Borg
- Backup.
-
- It runs on Linux, MacOS and Windows (via Windowsโ Linux
- Subsystem (WSL)). Find the right way to install it{' '}
-
- just here
-
- .
-
-
-
- );
-}
-
-export default WizardStep1;
diff --git a/Components/WizardSteps/WizardStep1/WizardStep1.module.css b/Components/WizardSteps/WizardStep1/WizardStep1.module.css
index e9ec3f5..b62ae36 100644
--- a/Components/WizardSteps/WizardStep1/WizardStep1.module.css
+++ b/Components/WizardSteps/WizardStep1/WizardStep1.module.css
@@ -1,137 +1,137 @@
.container {
- margin: 40px 20px 20px 5px;
- box-shadow:
- 0 1px 3px rgba(0, 0, 0, 0.12),
- 0 1px 2px rgba(0, 0, 0, 0.24);
- border-radius: 5px;
- text-align: left;
- padding: 30px 70px;
- animation: animStep ease-in 0.3s 1 normal none;
+ margin: 40px 20px 20px 5px;
+ box-shadow:
+ 0 1px 3px rgba(0, 0, 0, 0.12),
+ 0 1px 2px rgba(0, 0, 0, 0.24);
+ border-radius: 5px;
+ text-align: left;
+ padding: 30px 70px;
+ animation: animStep ease-in 0.3s 1 normal none;
}
@keyframes animStep {
- 0% {
- opacity: 0;
- }
+ 0% {
+ opacity: 0;
+ }
- 50% {
- opacity: 0.5;
- }
+ 50% {
+ opacity: 0.5;
+ }
- 100% {
- opacity: 1;
- }
+ 100% {
+ opacity: 1;
+ }
}
.container h1 {
- text-align: center;
- font-size: 1.7em;
- padding-bottom: 20px;
- display: flex;
- align-items: center;
+ text-align: center;
+ font-size: 1.7em;
+ padding-bottom: 20px;
+ display: flex;
+ align-items: center;
}
.container a {
- color: #6d4aff;
+ color: #6d4aff;
}
.separator {
- height: 15px;
+ height: 15px;
}
.container {
- line-height: 1.8em;
- font-size: 1.05em;
+ line-height: 1.8em;
+ font-size: 1.05em;
}
h1 .icon {
- color: #6d4aff;
- margin-right: 5px;
+ color: #6d4aff;
+ margin-right: 5px;
}
.container img {
- max-width: 650px;
+ max-width: 650px;
}
.code {
- background-color: #111827;
- color: #f8f8f2;
- font-family:
- ui-monospace,
- SFMono-Regular,
- Menlo,
- Monaco,
- Consolas,
- liberation mono,
- courier new,
- monospace;
- padding: 5px 15px;
- border-radius: 5px;
- display: inline-block;
- margin: 5px 0px 10px 0px;
- font-size: 0.8em;
- white-space: pre;
- line-height: 1em;
+ background-color: #111827;
+ color: #f8f8f2;
+ font-family:
+ ui-monospace,
+ SFMono-Regular,
+ Menlo,
+ Monaco,
+ Consolas,
+ liberation mono,
+ courier new,
+ monospace;
+ padding: 5px 15px;
+ border-radius: 5px;
+ display: inline-block;
+ margin: 5px 0px 10px 0px;
+ font-size: 0.8em;
+ white-space: pre;
+ line-height: 1em;
}
.verifyOrange {
- background-color: #ff7a1b;
- color: #fff;
- font-weight: 500;
- border-radius: 5px;
- padding: 15px;
+ background-color: #ff7a1b;
+ color: #fff;
+ font-weight: 500;
+ border-radius: 5px;
+ padding: 15px;
}
.verifyRed {
- background-color: #ea1313;
- color: #fff;
- font-weight: 500;
- border-radius: 5px;
- padding: 15px;
- margin-top: 10px;
+ background-color: #ea1313;
+ color: #fff;
+ font-weight: 500;
+ border-radius: 5px;
+ padding: 15px;
+ margin-top: 10px;
}
.verifyOrange li,
.container li {
- list-style: disc;
- margin-top: 1px;
- margin-left: 30px;
+ list-style: disc;
+ margin-top: 1px;
+ margin-left: 30px;
}
.verifyOrange li .sshPublicKey {
- background-color: #282a36;
- font-family:
- ui-monospace,
- SFMono-Regular,
- Menlo,
- Monaco,
- Consolas,
- liberation mono,
- courier new,
- monospace;
- border-radius: 5px;
- padding: 5px;
+ background-color: #282a36;
+ font-family:
+ ui-monospace,
+ SFMono-Regular,
+ Menlo,
+ Monaco,
+ Consolas,
+ liberation mono,
+ courier new,
+ monospace;
+ border-radius: 5px;
+ padding: 5px;
}
.verifyRed .alert,
.verifyOrange .alert {
- text-align: center;
- font-size: 1.1em;
- padding-bottom: 10px;
- display: flex;
- align-items: center;
+ text-align: center;
+ font-size: 1.1em;
+ padding-bottom: 10px;
+ display: flex;
+ align-items: center;
}
.iconAlert {
- margin-right: 5px;
+ margin-right: 5px;
}
.note {
- font-style: italic;
- color: #494b7a4d;
- font-size: 0.8em;
+ font-style: italic;
+ color: #494b7a4d;
+ font-size: 0.8em;
}
.note a {
- color: #6d4aff73;
+ color: #6d4aff73;
}
diff --git a/Components/WizardSteps/WizardStep1/WizardStep1.tsx b/Components/WizardSteps/WizardStep1/WizardStep1.tsx
new file mode 100644
index 0000000..d417ddc
--- /dev/null
+++ b/Components/WizardSteps/WizardStep1/WizardStep1.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import classes from '../WizardStep1/WizardStep1.module.css';
+import { IconDeviceDesktopAnalytics, IconTerminal2 } from '@tabler/icons-react';
+
+function WizardStep1() {
+ return (
+
+
+
+ Command Line Interface
+
+
+ We recommend using the official
BorgBackup client which is supported by most Linux
+ distributions.
+
+ More information about installation can be{' '}
+
+ found here
+
+ .
+ To
automate your backup , you can also use{' '}
+
+ Borgmatic
+ {' '}
+ which is a{' '}
+
+ Debian package
+
+ . On the step 4, you will find a pattern of default config.
+
+
+
+
+ Graphical User Interface
+
+
+ BorgWarehouse is
compatible with all BorgBackup graphical clients , including the
+ well-known{' '}
+
+ Pika Backup
+ {' '}
+ and{' '}
+
+ Vorta
+
+ .
+
+
+ );
+}
+
+export default WizardStep1;
diff --git a/Components/WizardSteps/WizardStep2/WizardStep2.js b/Components/WizardSteps/WizardStep2/WizardStep2.js
deleted file mode 100644
index 5963eaf..0000000
--- a/Components/WizardSteps/WizardStep2/WizardStep2.js
+++ /dev/null
@@ -1,149 +0,0 @@
-//Lib
-import React from 'react';
-import classes from '../WizardStep1/WizardStep1.module.css';
-import { IconTool, IconAlertCircle } from '@tabler/icons-react';
-import CopyButton from '../../UI/CopyButton/CopyButton';
-import lanCommandOption from '../../../helpers/functions/lanCommandOption';
-
-function WizardStep2(props) {
- ////Vars
- const wizardEnv = props.wizardEnv;
- const UNIX_USER = wizardEnv.UNIX_USER;
- //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
- const { FQDN, SSH_SERVER_PORT } = lanCommandOption(
- wizardEnv,
- props.selectedOption.lanCommand
- );
-
- return (
-
-
-
- Initialize a repository
-
-
- To initialize your repository with borgbackup :
-
-
-
- borg init -e repokey-blake2 ssh://
- {UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
-
-
-
-
- The encryption mode is the one recommended by BorgBackup.
- For more information,{' '}
-
- click here
-
- .
-
-
-
-
-
Borgmatic
-
- If you are using Borgmatic and have
already edited the configuration file
- (find a sample on the step 4) :
-
-
-
- borgmatic init -e repokey-blake2
-
-
-
-
-
-
Vorta
-
- To "Initialize a new repository" or "Add existing repository",
- copy this into the field "Repository URL" of Vorta :
-
-
-
- ssh://
- {UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
-
-
-
- For more information about the Vorta graphical client, please
- refer to{' '}
-
- this documentation
-
- .
-
-
-
-
-
-
- Check the fingerprint of server
-
- To check that you are talking to the right server, please make
- sure to validate one of the following key's fingerprint when you
- first connect :
-
-
- ECDSA : {wizardEnv.SSH_SERVER_FINGERPRINT_ECDSA}
-
-
-
-
- ED25519 : {wizardEnv.SSH_SERVER_FINGERPRINT_ED25519}
-
-
-
-
- RSA : {wizardEnv.SSH_SERVER_FINGERPRINT_RSA}
-
-
-
-
-
-
-
- Save your passphrase
-
- Once again, the server cannot access your encrypted backup data
- or the encryption passphrase. Remember to put your passphrase in
- your password manager when you initialise your repository.
-
-
- );
-}
-
-export default WizardStep2;
diff --git a/Components/WizardSteps/WizardStep2/WizardStep2.tsx b/Components/WizardSteps/WizardStep2/WizardStep2.tsx
new file mode 100644
index 0000000..e4f760b
--- /dev/null
+++ b/Components/WizardSteps/WizardStep2/WizardStep2.tsx
@@ -0,0 +1,132 @@
+import { IconAlertCircle, IconTool } from '@tabler/icons-react';
+import CopyButton from '../../UI/CopyButton/CopyButton';
+import { WizardStepProps } from '~/types';
+import classes from '../WizardStep1/WizardStep1.module.css';
+import { lanCommandOption } from '~/helpers/functions';
+
+function WizardStep2(props: WizardStepProps) {
+ const wizardEnv = props.wizardEnv;
+ const UNIX_USER = wizardEnv?.UNIX_USER;
+ //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
+ const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
+
+ return (
+
+
+
+ Initialize a repository
+
+
+ To initialize your repository with borgbackup :
+
+
+
+ borg init -e repokey-blake2 ssh://
+ {UNIX_USER}@{FQDN}
+ {SSH_SERVER_PORT}/./
+ {props.selectedRepo?.repositoryName}
+
+
+
+
+ The encryption mode is the one recommended by BorgBackup. For more information,{' '}
+
+ click here
+
+ .
+
+
+
+
+
Borgmatic
+
+ If you are using Borgmatic and have
already edited the configuration file (find a
+ sample on the step 4) :
+
+
+
borgmatic init -e repokey-blake2
+
+
+
+
+
Pika, Vorta...
+
+ To "Initialize a new repository" or "Add existing repository", copy this
+ into the field "Repository URL" of your graphical client :
+
+
+
+ ssh://
+ {UNIX_USER}@{FQDN}
+ {SSH_SERVER_PORT}/./
+ {props.selectedRepo?.repositoryName}
+
+
+
+
+
+
+
+
+
+ Check the fingerprint of server
+
+ To check that you are talking to the right server, please make sure to validate one of the
+ following key's fingerprint when you first connect :
+
+
+ ECDSA : {wizardEnv?.SSH_SERVER_FINGERPRINT_ECDSA}
+
+
+
+
+ ED25519 : {wizardEnv?.SSH_SERVER_FINGERPRINT_ED25519}
+
+
+
+
+ RSA : {wizardEnv?.SSH_SERVER_FINGERPRINT_RSA}
+
+
+
+
+
+
+
+ Save your passphrase
+
+ Once again, the server cannot access your encrypted backup data or the encryption
+ passphrase. Remember to put your passphrase in your password manager when you initialise
+ your repository.
+
+
+ );
+}
+
+export default WizardStep2;
diff --git a/Components/WizardSteps/WizardStep3/WizardStep3.js b/Components/WizardSteps/WizardStep3/WizardStep3.js
deleted file mode 100644
index cb30a5c..0000000
--- a/Components/WizardSteps/WizardStep3/WizardStep3.js
+++ /dev/null
@@ -1,184 +0,0 @@
-//Lib
-import React from 'react';
-import classes from '../WizardStep1/WizardStep1.module.css';
-import { IconChecks, IconPlayerPlay } from '@tabler/icons-react';
-import CopyButton from '../../UI/CopyButton/CopyButton';
-import lanCommandOption from '../../../helpers/functions/lanCommandOption';
-
-function WizardStep3(props) {
- ////Vars
- const wizardEnv = props.wizardEnv;
- const UNIX_USER = wizardEnv.UNIX_USER;
- //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
- const { FQDN, SSH_SERVER_PORT } = lanCommandOption(
- wizardEnv,
- props.selectedOption.lanCommand
- );
-
- return (
-
-
-
- Launch a backup
-
-
- To launch a backup with borgbackup :
-
-
- borg create ssh://
- {UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
- ::archive1 /your/pathToBackup
-
-
-
-
-
-
-
- Check your backup{' '}
-
- (always)
-
-
-
- BorgWarehouse only stores your backups. They are
- encrypted and there is no way for BorgWarehouse to know
- if the backup is intact.
-
- You should regularly test your backups and check that the data
- is recoverable.{' '}
-
- BorgWarehouse cannot do this for you and does not guarantee
- anything.
-
-
-
-
-
- Based on the Borg documentation, you have multiple ways to check
- that your backups are correct with your tools (tar, rsync, diff
- or other tools).
-
- Check the integrity of a repository with :
-
-
- borg check -v --progress ssh://
- {UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
-
-
-
- List the remote archives with :
-
-
- borg list ssh://
- {UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
-
-
-
- Download a remote archive with the following command :
-
-
- borg export-tar --tar-filter="gzip -9" ssh://
- {UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
- ::archive1 archive1.tar.gz
-
-
-
-
- Mount an archive to compare or backup some files without
- download all the archive :
-
-
-
- borg mount ssh://
- {UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
- {props.selectedOption.repositoryName}
- ::archive1 /tmp/yourMountPoint
-
-
-
-
- To verify the consistency of a repository and the corresponding
- archives, please refer to{' '}
-
- this documentation
-
-
-
-
Borgmatic
-
- If you are using Borgmatic, please refer to{' '}
-
- this documentation
- {' '}
- for a consistency check.
-
-
Vorta
-
-
- );
-}
-
-export default WizardStep3;
diff --git a/Components/WizardSteps/WizardStep3/WizardStep3.tsx b/Components/WizardSteps/WizardStep3/WizardStep3.tsx
new file mode 100644
index 0000000..1b065f1
--- /dev/null
+++ b/Components/WizardSteps/WizardStep3/WizardStep3.tsx
@@ -0,0 +1,174 @@
+import React from 'react';
+import classes from '../WizardStep1/WizardStep1.module.css';
+import { IconChecks, IconPlayerPlay } from '@tabler/icons-react';
+import CopyButton from '../../UI/CopyButton/CopyButton';
+import { WizardStepProps } from '~/types';
+import { lanCommandOption } from '~/helpers/functions';
+
+function WizardStep3(props: WizardStepProps) {
+ const wizardEnv = props.wizardEnv;
+ const UNIX_USER = wizardEnv?.UNIX_USER;
+ //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
+ const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
+
+ return (
+
+
+
+ Launch a backup
+
+
+ To launch a backup with borgbackup :
+
+
+ borg create ssh://
+ {UNIX_USER}@{FQDN}
+ {SSH_SERVER_PORT}/./
+ {props.selectedRepo?.repositoryName}
+ ::archive1 /your/pathToBackup
+
+
+
+
+
+
+
+ Check your backup{' '}
+ (always)
+
+
+ BorgWarehouse only stores your backups. They are encrypted and there is no way {' '}
+ for BorgWarehouse to know if the backup is intact.
+
+ You should regularly test your backups and check that the data is recoverable.{' '}
+ BorgWarehouse cannot do this for you and does not guarantee anything.
+
+
+
+
+ Based on the Borg documentation, you have multiple ways to check that your backups are
+ correct with your tools (tar, rsync, diff or other tools).
+
+ Check the integrity of a repository with :
+
+
+ borg check -v --progress ssh://
+ {UNIX_USER}@{FQDN}
+ {SSH_SERVER_PORT}/./
+ {props.selectedRepo?.repositoryName}
+
+
+
+ List the remote archives with :
+
+
+ borg list ssh://
+ {UNIX_USER}@{FQDN}
+ {SSH_SERVER_PORT}/./
+ {props.selectedRepo?.repositoryName}
+
+
+
+ Download a remote archive with the following command :
+
+
+ borg export-tar --tar-filter="gzip -9" ssh://
+ {UNIX_USER}@{FQDN}
+ {SSH_SERVER_PORT}/./
+ {props.selectedRepo?.repositoryName}
+ ::archive1 archive1.tar.gz
+
+
+
+ Mount an archive to compare or backup some files without download all the archive :
+
+
+ borg mount ssh://
+ {UNIX_USER}@{FQDN}
+ {SSH_SERVER_PORT}/./
+ {props.selectedRepo?.repositoryName}
+ ::archive1 /tmp/yourMountPoint
+
+
+
+
+ To verify the consistency of a repository and the corresponding archives, please refer to{' '}
+
+ this documentation
+
+
+
+
Borgmatic
+
+ If you are using Borgmatic, please refer to{' '}
+
+ this documentation
+ {' '}
+ for a consistency check.
+
+
Pika, Vorta...
+
+
+ );
+}
+
+export default WizardStep3;
diff --git a/Components/WizardSteps/WizardStep4/WizardStep4.js b/Components/WizardSteps/WizardStep4/WizardStep4.js
deleted file mode 100644
index 99dcc8d..0000000
--- a/Components/WizardSteps/WizardStep4/WizardStep4.js
+++ /dev/null
@@ -1,125 +0,0 @@
-//Lib
-import React from 'react';
-import classes from '../WizardStep1/WizardStep1.module.css';
-import { IconWand } from '@tabler/icons-react';
-import CopyButton from '../../UI/CopyButton/CopyButton';
-import lanCommandOption from '../../../helpers/functions/lanCommandOption';
-
-function WizardStep4(props) {
- ////Vars
- const wizardEnv = props.wizardEnv;
- const UNIX_USER = wizardEnv.UNIX_USER;
- //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
- const { FQDN, SSH_SERVER_PORT } = lanCommandOption(
- wizardEnv,
- props.selectedOption.lanCommand
- );
-
- const configBorgmatic = `location:
- # List of source directories to backup.
- source_directories:
- - /your-repo-to-backup
- - /another/repo-to-backup
-
-repositories:
- # Paths of local or remote repositories to backup to.
- - ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}
-
-storage:
- archive_name_format: '{FQDN}-documents-{now}'
- encryption_passphrase: "YOUR PASSPHRASE"
-
-retention:
- # Retention policy for how many backups to keep.
- keep_daily: 7
- keep_weekly: 4
- keep_monthly: 6
-
-consistency:
- # List of checks to run to validate your backups.
- checks:
- - name: repository
- - name: archives
- frequency: 2 weeks
-
-#hooks:
- # Custom preparation scripts to run.
- #before_backup:
- # - prepare-for-backup.sh
-
- # Databases to dump and include in backups.
- #postgresql_databases:
- # - name: users
-
- # Third-party services to notify you if backups aren't happening.
- #healthchecks: https://hc-ping.com/be067061-cf96-4412-8eae-62b0c50d6a8c`;
-
- return (
-
-
-
- Automate your backup
-
-
- The official borgbackup project provides a script in its
- documentation
-
- right here
-
- .
-
-
-
-
Vorta
-
-
-
Borgmatic
-
- If you are using Borgmatic, you can check
-
- this documentation
-
- and
adapt and use the following script :
-
-
-
{configBorgmatic}
-
-
-
-
-
- );
-}
-
-export default WizardStep4;
diff --git a/Components/WizardSteps/WizardStep4/WizardStep4.tsx b/Components/WizardSteps/WizardStep4/WizardStep4.tsx
new file mode 100644
index 0000000..17d355a
--- /dev/null
+++ b/Components/WizardSteps/WizardStep4/WizardStep4.tsx
@@ -0,0 +1,125 @@
+import React from 'react';
+import classes from '../WizardStep1/WizardStep1.module.css';
+import { IconWand } from '@tabler/icons-react';
+import CopyButton from '../../UI/CopyButton/CopyButton';
+import { WizardStepProps } from '~/types';
+import { lanCommandOption } from '~/helpers/functions';
+
+function WizardStep4(props: WizardStepProps) {
+ const wizardEnv = props.wizardEnv;
+ const UNIX_USER = wizardEnv?.UNIX_USER;
+ //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
+ const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
+
+ const configBorgmatic = `
+ # List of source directories to backup.
+ source_directories:
+ - /your-repo-to-backup
+ - /another/repo-to-backup
+
+repositories:
+ # Paths of local or remote repositories to backup to.
+ - path: ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}
+
+archive_name_format: '{FQDN}-documents-{now}'
+encryption_passphrase: "YOUR PASSPHRASE"
+
+# Retention policy for how many backups to keep.
+keep_daily: 7
+keep_weekly: 4
+keep_monthly: 6
+
+# List of checks to run to validate your backups.
+checks:
+ - name: repository
+ - name: archives
+ frequency: 2 weeks
+
+#hooks:
+ # Custom preparation scripts to run.
+ #before_backup:
+ # - prepare-for-backup.sh
+
+ # Databases to dump and include in backups.
+ #postgresql_databases:
+ # - name: users
+
+ # Third-party services to notify you if backups aren't happening.
+ #healthchecks: https://hc-ping.com/be067061-cf96-4412-8eae-62b0c50d6a8c`;
+
+ return (
+
+
+
+ Automate your backup
+
+
+ The official borgbackup project provides a script in its documentation
+
+ right here
+
+ .
+
+
+
+
Pika, Vorta...
+
+
+
Borgmatic
+
+ If you are using Borgmatic, you can check
+
+ this documentation
+
+ and
adapt and use the following script :
+
+
+
{configBorgmatic}
+
+
+
+
+
+ );
+}
+
+export default WizardStep4;
diff --git a/Components/WizardSteps/WizardStepBar/WizardStepBar.js b/Components/WizardSteps/WizardStepBar/WizardStepBar.js
deleted file mode 100644
index f79e48e..0000000
--- a/Components/WizardSteps/WizardStepBar/WizardStepBar.js
+++ /dev/null
@@ -1,103 +0,0 @@
-//Lib
-import React from 'react';
-import classes from './WizardStepBar.module.css';
-import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
-
-function WizardStepBar(props) {
- ////Functions
- //Color onClick on a step
- const colorHandler = (step) => {
- if (step <= props.step) {
- return classes.active;
- } else {
- return classes.inactive;
- }
- };
- //Color onClick on next step button
- const colorChevronNextStep = () => {
- if (props.step < 4) {
- return classes.activeChevron;
- } else {
- return classes.inactiveChevron;
- }
- };
- //Color onClick on previous step button
- const colorChevronPreviousStep = () => {
- if (props.step > 1) {
- return classes.activeChevron;
- } else {
- return classes.inactiveChevron;
- }
- };
-
- return (
-
-
-
- props.setStep(1)}
- >
-
- 1
-
-
- Client Setup
-
-
-
- props.setStep(2)}
- >
-
- 2
-
-
- Init. repository
-
-
-
- props.setStep(3)}
- >
-
- 3
-
-
- Launch & Verify
-
-
-
- props.setStep(4)}>
-
- 4
-
-
- Automation
-
-
-
-
-
- );
-}
-
-export default WizardStepBar;
diff --git a/Components/WizardSteps/WizardStepBar/WizardStepBar.module.css b/Components/WizardSteps/WizardStepBar/WizardStepBar.module.css
index b5453c2..434e2c7 100644
--- a/Components/WizardSteps/WizardStepBar/WizardStepBar.module.css
+++ b/Components/WizardSteps/WizardStepBar/WizardStepBar.module.css
@@ -1,94 +1,94 @@
/* General */
.stepBarContainer {
- text-transform: uppercase;
- color: #494b7a;
- font-size: 1em;
- margin-top: 40px;
- display: flex;
- justify-content: center;
- align-items: center;
- text-align: center;
+ text-transform: uppercase;
+ color: #494b7a;
+ font-size: 1em;
+ margin-top: 40px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
}
.stepBarContainer li {
- display: inline-block;
- position: relative;
- margin: 0px 30px;
- width: 180px;
- cursor: pointer;
+ display: inline-block;
+ position: relative;
+ margin: 0px 30px;
+ width: 180px;
+ cursor: pointer;
}
.stepBarContainer ul {
- padding-top: 10px;
- margin: 0;
- padding-inline-start: 0px;
+ padding-top: 10px;
+ margin: 0;
+ padding-inline-start: 0px;
}
/* Transition Active / Inactive */
.number {
- display: inline-block;
- position: relative;
- color: #494b7a4d;
- background: #fff;
- border: 1px solid #494b7a4d;
- border-radius: 50%;
- width: 37px;
- padding: 8px 5px;
- margin-bottom: 4px;
- font-weight: 700;
- z-index: 1;
+ display: inline-block;
+ position: relative;
+ color: #494b7a4d;
+ background: #fff;
+ border: 1px solid #494b7a4d;
+ border-radius: 50%;
+ width: 37px;
+ padding: 8px 5px;
+ margin-bottom: 4px;
+ font-weight: 700;
+ z-index: 1;
}
.active.number {
- box-shadow: 0 0 15px 5px rgba(110, 74, 255, 0.405);
- color: #fff;
- background: #6d4aff;
- transition: all 1000ms cubic-bezier(0.25, 0.25, 0.75, 0.75);
- transition-delay: 500ms;
+ box-shadow: 0 0 15px 5px rgba(110, 74, 255, 0.405);
+ color: #fff;
+ background: #6d4aff;
+ transition: all 1000ms cubic-bezier(0.25, 0.25, 0.75, 0.75);
+ transition-delay: 500ms;
}
.text {
- padding: 8px 0 0 0;
- color: #494b7a4d;
+ padding: 8px 0 0 0;
+ color: #494b7a4d;
}
.active.text {
- transition: all 1000ms cubic-bezier(0.25, 0.25, 0.75, 0.75);
- transition-delay: 500ms;
- color: #494b7a;
+ transition: all 1000ms cubic-bezier(0.25, 0.25, 0.75, 0.75);
+ transition-delay: 500ms;
+ color: #494b7a;
}
.line {
- background: #6d4aff;
- width: 0;
- height: 2px;
- position: absolute;
- top: 17px;
- left: 108px;
- z-index: 0;
- transition: all 500ms cubic-bezier(0.25, 0.25, 0.75, 0.75);
+ background: #6d4aff;
+ width: 0;
+ height: 2px;
+ position: absolute;
+ top: 17px;
+ left: 108px;
+ z-index: 0;
+ transition: all 500ms cubic-bezier(0.25, 0.25, 0.75, 0.75);
}
.active .line {
- background: #6d4aff;
- width: 204px;
- height: 2px;
- position: absolute;
- top: 17px;
- left: 108px;
- z-index: 0;
+ background: #6d4aff;
+ width: 204px;
+ height: 2px;
+ position: absolute;
+ top: 17px;
+ left: 108px;
+ z-index: 0;
}
.activeChevron {
- cursor: pointer;
+ cursor: pointer;
}
.activeChevron:hover {
- color: #6d4aff;
+ color: #6d4aff;
}
.inactiveChevron {
- color: #494b7a4d;
+ color: #494b7a4d;
}
diff --git a/Components/WizardSteps/WizardStepBar/WizardStepBar.tsx b/Components/WizardSteps/WizardStepBar/WizardStepBar.tsx
new file mode 100644
index 0000000..a6adb7c
--- /dev/null
+++ b/Components/WizardSteps/WizardStepBar/WizardStepBar.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import classes from './WizardStepBar.module.css';
+import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
+
+type WizardStepBarProps = {
+ step: number;
+ setStep: (step: number) => void;
+ previousStepHandler: () => void;
+ nextStepHandler: () => void;
+};
+
+function WizardStepBar(props: WizardStepBarProps) {
+ //Color onClick on a step
+ const colorHandler = (step: number) => {
+ if (step <= props.step) {
+ return classes.active;
+ } else {
+ return classes.inactive;
+ }
+ };
+ //Color onClick on next step button
+ const colorChevronNextStep = () => {
+ if (props.step < 4) {
+ return classes.activeChevron;
+ } else {
+ return classes.inactiveChevron;
+ }
+ };
+ //Color onClick on previous step button
+ const colorChevronPreviousStep = () => {
+ if (props.step > 1) {
+ return classes.activeChevron;
+ } else {
+ return classes.inactiveChevron;
+ }
+ };
+
+ return (
+
+
+
+ props.setStep(1)}>
+ 1
+ Client Setup
+
+
+ props.setStep(2)}>
+ 2
+ Init. repository
+
+
+ props.setStep(3)}>
+ 3
+ Launch & Verify
+
+
+ props.setStep(4)}>
+ 4
+ Automation
+
+
+
+
+ );
+}
+
+export default WizardStepBar;
diff --git a/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.js b/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.js
deleted file mode 100644
index 8a4b153..0000000
--- a/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.js
+++ /dev/null
@@ -1,94 +0,0 @@
-//Lib
-import {
- Chart as ChartJS,
- CategoryScale,
- LinearScale,
- BarElement,
- Title,
- Tooltip,
- Legend,
-} from 'chart.js';
-import { Bar } from 'react-chartjs-2';
-import { useState, useEffect } from 'react';
-
-export default function StorageUsedChartBar() {
- //States
- const [data, setData] = useState([]);
-
- //LifeCycle
- useEffect(() => {
- const dataFetch = async () => {
- try {
- const response = await fetch('/api/repo', {
- method: 'GET',
- headers: {
- 'Content-type': 'application/json',
- },
- });
- setData((await response.json()).repoList);
- } catch (error) {
- console.log('Fetching datas error');
- }
- };
-
- dataFetch();
- }, []);
-
- ////Chart.js
- ChartJS.register(
- CategoryScale,
- LinearScale,
- BarElement,
- Title,
- Tooltip,
- Legend
- );
-
- const options = {
- responsive: true,
- plugins: {
- legend: {
- position: 'bottom',
- },
- title: {
- position: 'bottom',
- display: true,
- text: 'Storage used for each repository',
- },
- },
- scales: {
- y: {
- max: 100,
- min: 0,
- ticks: {
- // Include a dollar sign in the ticks
- callback: function (value) {
- return value + '%';
- },
- stepSize: 10,
- },
- },
- },
- };
-
- const labels = data.map((repo) => repo.alias);
-
- const dataChart = {
- labels,
- datasets: [
- {
- label: 'Storage used (%)',
- //storageUsed is in octet, storageSize is in GB. Round to 1 decimal for %.
- data: data.map((repo) =>
- (
- ((repo.storageUsed / 1000000) * 100) /
- repo.storageSize
- ).toFixed(1)
- ),
- backgroundColor: '#704dff',
- },
- ],
- };
-
- return ;
-}
diff --git a/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.tsx b/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.tsx
new file mode 100644
index 0000000..3a9bd86
--- /dev/null
+++ b/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar.tsx
@@ -0,0 +1,82 @@
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend,
+} from 'chart.js';
+import { Bar } from 'react-chartjs-2';
+import { useState, useEffect } from 'react';
+import { Repository, Optional } from '~/types';
+
+export default function StorageUsedChartBar() {
+ const [data, setData] = useState>>();
+
+ useEffect(() => {
+ const dataFetch = async () => {
+ try {
+ const response = await fetch('/api/v1/repositories', {
+ method: 'GET',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ });
+ setData((await response.json()).repoList);
+ } catch (error) {
+ console.log('Fetching datas error');
+ }
+ };
+
+ dataFetch();
+ }, []);
+
+ ////Chart.js
+ ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
+
+ const options = {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'bottom' as const,
+ },
+ title: {
+ position: 'bottom' as const,
+ display: true,
+ text: 'Storage used for each repository',
+ },
+ },
+ scales: {
+ y: {
+ max: 100,
+ min: 0,
+ ticks: {
+ // Include a dollar sign in the ticks
+ callback: function (value: number | string) {
+ return value + '%';
+ },
+ stepSize: 10,
+ },
+ },
+ },
+ };
+
+ const labels = data?.map((repo) => repo.alias);
+
+ const dataChart = {
+ labels,
+ datasets: [
+ {
+ label: 'Storage used (%)',
+ //storageUsed is in kB, storageSize is in GB. Round to 1 decimal for %.
+ data: data?.map((repo) =>
+ (((repo.storageUsed / 1024 ** 2) * 100) / repo.storageSize).toFixed(1)
+ ),
+ backgroundColor: '#704dff',
+ },
+ ],
+ };
+
+ return ;
+}
diff --git a/Containers/RepoList/RepoList.js b/Containers/RepoList/RepoList.js
deleted file mode 100644
index 602be5a..0000000
--- a/Containers/RepoList/RepoList.js
+++ /dev/null
@@ -1,171 +0,0 @@
-//Lib
-import classes from './RepoList.module.css';
-import { useState, useEffect } from 'react';
-import { IconPlus } from '@tabler/icons-react';
-import { useRouter } from 'next/router';
-import Link from 'next/link';
-import useSWR, { useSWRConfig } from 'swr';
-import { ToastContainer, toast } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-
-//Composants
-import Repo from '../../Components/Repo/Repo';
-import RepoManage from '../RepoManage/RepoManage';
-import ShimmerRepoList from '../../Components/UI/ShimmerRepoList/ShimmerRepoList';
-
-export default function RepoList() {
- ////Var
- const router = useRouter();
- const { mutate } = useSWRConfig();
- const toastOptions = {
- position: 'top-right',
- autoClose: 8000,
- hideProgressBar: false,
- closeOnClick: true,
- pauseOnHover: true,
- draggable: true,
- progress: undefined,
- };
-
- ////Datas
- //Write a fetcher function to wrap the native fetch function and return the result of a call to url in json format
- const fetcher = async (url) => await fetch(url).then((res) => res.json());
- const { data, error } = useSWR('/api/repo', fetcher);
-
- ////LifeCycle
- //Component did mount
- useEffect(() => {
- //If the route is home/manage-repo/add, open the RepoAdd box.
- if (router.pathname === '/manage-repo/add') {
- setDisplayRepoAdd(!displayRepoAdd);
- }
- //If the route is home/manage-repo/edit, open the RepoAdd box.
- if (router.pathname.startsWith('/manage-repo/edit')) {
- setDisplayRepoEdit(!displayRepoEdit);
- }
- //Fetch wizardEnv to hydrate Repo components
- const fetchWizardEnv = async () => {
- try {
- const response = await fetch('/api/account/getWizardEnv', {
- method: 'GET',
- headers: {
- 'Content-type': 'application/json',
- },
- });
- setWizardEnv((await response.json()).wizardEnv);
- } catch (error) {
- console.log('Fetching datas error');
- }
- };
- fetchWizardEnv();
- }, []);
-
- ////States
- const [displayRepoAdd, setDisplayRepoAdd] = useState(false);
- const [displayRepoEdit, setDisplayRepoEdit] = useState(false);
- const [wizardEnv, setWizardEnv] = useState({});
-
- ////Functions
-
- //Firstly, check the availability of data and condition it.
- if (!data) {
- //Force mutate after login (force a API GET on /api/repo to load repoList)
- mutate('/api/repo');
- return ;
- }
- if (error) {
- toast.error('An error has occurred.', toastOptions);
- return ;
- }
- if (data.status == 500) {
- toast.error('API Error !', toastOptions);
- return ;
- }
-
- //BUTTON : Display RepoManage component box for ADD
- const manageRepoAddHandler = () => {
- router.replace('/manage-repo/add');
- };
-
- //BUTTON : Display RepoManage component box for EDIT
- const repoManageEditHandler = (id) => {
- router.replace('/manage-repo/edit/' + id);
- };
-
- //BUTTON : Close RepoManage component box (when cross is clicked)
- const closeRepoManageBoxHandler = () => {
- router.replace('/');
- };
-
- // UI EFFECT : Display blur when display add repo modale
- const displayBlur = () => {
- if (displayRepoAdd || displayRepoEdit) {
- return classes.containerBlur;
- } else {
- return classes.container;
- }
- };
-
- //Dynamic list of repositories (with a map of Repo components)
- const renderRepoList = data.repoList.map((repo, index) => {
- return (
- <>
- repoManageEditHandler(repo.id)}
- wizardEnv={wizardEnv}
- >
- >
- );
- });
-
- return (
- <>
-
-
-
-
- Add a repository
-
-
-
-
- {displayRepoAdd ? (
-
- ) : null}
- {displayRepoEdit ? (
-
- ) : null}
- >
- );
-}
diff --git a/Containers/RepoList/RepoList.module.css b/Containers/RepoList/RepoList.module.css
index 2187a7d..087d90b 100644
--- a/Containers/RepoList/RepoList.module.css
+++ b/Containers/RepoList/RepoList.module.css
@@ -1,125 +1,198 @@
.container {
- display: flex;
- justify-content: center;
- flex-direction: column;
- width: 100%;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ width: 100%;
}
.containerBlur {
- display: flex;
- justify-content: center;
- flex-direction: column;
- width: 100%;
- filter: blur(3px);
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ width: 100%;
+ filter: blur(3px);
}
.containerRepoList {
- display: flex;
- flex-direction: row;
+ display: flex;
+ flex-direction: row;
}
.containerAddRepo {
- display: flex;
- flex-direction: row;
- margin: 20px auto;
- width: 90%;
+ display: flex;
+ flex-direction: row;
+ margin: 20px auto;
+ width: 90%;
}
.newRepoButton {
- position: relative;
- margin: auto;
- padding: 19px 22px;
- transition: all 0.2s ease;
- display: flex;
- justify-content: center;
- align-items: center;
- cursor: pointer;
- text-decoration: none;
+ position: relative;
+ margin: auto;
+ padding: 19px 22px;
+ transition: all 0.2s ease;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ text-decoration: none;
}
.newRepoButton:before {
- content: '';
- position: absolute;
- top: 6px;
- left: 3px;
- display: block;
- border-radius: 28px;
- background: #6d4aff;
- width: 50px;
- height: 50px;
- transition: all 0.3s ease;
+ content: '';
+ position: absolute;
+ top: 6px;
+ left: 3px;
+ display: block;
+ border-radius: 28px;
+ background: #6d4aff;
+ width: 50px;
+ height: 50px;
+ transition: all 0.3s ease;
}
.newRepoButton span {
- position: relative;
- font-size: 16px;
- line-height: 18px;
- font-weight: 600;
- vertical-align: middle;
- padding-left: 15px;
- color: #494b7a;
+ position: relative;
+ font-size: 16px;
+ line-height: 18px;
+ font-weight: 600;
+ vertical-align: middle;
+ padding-left: 15px;
+ color: #494b7a;
}
.newRepoButton .plusIcon {
- position: relative;
- top: 0;
- fill: none;
- stroke-linecap: round;
- stroke-linejoin: round;
- stroke: #fff;
- transform: translateX(-5px);
- transition: all 0.3s ease;
+ position: relative;
+ top: 0;
+ fill: none;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke: #fff;
+ transform: translateX(-5px);
+ transition: all 0.3s ease;
}
.newRepoButton:hover:before {
- width: 100%;
- background: #6d4aff;
+ width: 100%;
+ background: #6d4aff;
}
.newRepoButton:hover span {
- color: #fff;
+ color: #fff;
}
.newRepoButton:hover .plusIcon {
- transform: translateX(0);
+ transform: translateX(0);
}
.newRepoButton:active {
- transform: scale(0.96);
+ transform: scale(0.96);
}
.RepoList {
- display: flex;
- flex-direction: column;
- width: 90%;
- margin: 5px auto;
- padding: 15px;
+ display: flex;
+ flex-direction: column;
+ width: 90%;
+ margin: 5px auto;
}
.unfoldButton {
- cursor: pointer;
- position: sticky;
- color: #a6a6b8;
- padding-top: 49px;
- align-self: flex-start;
- top: 0;
+ cursor: pointer;
+ position: sticky;
+ color: #a6a6b8;
+ padding-top: 49px;
+ align-self: flex-start;
+ top: 0;
}
.foldButton {
- cursor: pointer;
- position: sticky;
- color: #a6a6b8;
- padding-top: 49px;
- align-self: flex-start;
- top: 0;
+ cursor: pointer;
+ position: sticky;
+ color: #a6a6b8;
+ padding-top: 49px;
+ align-self: flex-start;
+ top: 0;
}
.unfoldButton:active,
.foldButton:active {
- transform: scale(0.96);
+ transform: scale(0.96);
}
@media all and (max-width: 1000px) {
- .newRepoButton {
- display: none;
- }
- .chevron {
- display: none;
- }
- .containerAddRepo {
- display: none;
- }
+ .newRepoButton {
+ display: none;
+ }
+ .chevron {
+ display: none;
+ }
+ .containerAddRepo {
+ display: none;
+ }
+}
+
+/* Toolbar */
+
+.toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 90%;
+ margin: 20px auto 10px;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.searchInput {
+ padding: 10px 15px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ font-size: 14px;
+ width: 100%;
+ max-width: 300px;
+}
+
+.sortIcons {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.icon {
+ cursor: pointer;
+ color: #a6a6b8;
+ transition: transform 0.2s ease;
+}
+
+.icon:hover {
+ transform: scale(1.1);
+ color: #6d4aff;
+}
+
+.iconActive {
+ color: #6d4aff;
+ transform: scale(1.2);
+}
+
+.searchContainer {
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ max-width: 300px;
+}
+
+.searchInput {
+ width: 100%;
+ padding: 8px 32px 8px 12px;
+ border-radius: 8px;
+ border: 1px solid #ccc;
+ font-size: 14px;
+ outline: none;
+ background-color: white;
+}
+
+.clearButton {
+ position: absolute;
+ right: 8px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ color: #999;
+}
+
+.clearButton:hover {
+ color: #333;
}
diff --git a/Containers/RepoList/RepoList.tsx b/Containers/RepoList/RepoList.tsx
new file mode 100644
index 0000000..d9160f0
--- /dev/null
+++ b/Containers/RepoList/RepoList.tsx
@@ -0,0 +1,279 @@
+import classes from './RepoList.module.css';
+import React, { useState, useEffect } from 'react';
+import {
+ IconPlus,
+ IconSortAscendingLetters,
+ IconSortDescendingLetters,
+ IconSortAscending2,
+ IconSortDescending2,
+ IconDatabase,
+ IconX,
+ IconClock,
+ IconCalendarUp,
+ IconCalendarDown,
+ IconSortAscendingSmallBig,
+ IconSortDescendingSmallBig,
+ IconSortDescending2Filled,
+} from '@tabler/icons-react';
+import { useRouter } from 'next/router';
+import Link from 'next/link';
+import useSWR, { useSWRConfig } from 'swr';
+import { ToastContainer, ToastOptions, toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+
+import Repo from '~/Components/Repo/Repo';
+import RepoManage from '../RepoManage/RepoManage';
+import ShimmerRepoList from '~/Components/UI/ShimmerRepoList/ShimmerRepoList';
+import { Repository, WizardEnvType, Optional } from '~/types';
+
+type SortOption =
+ | 'alias-asc'
+ | 'alias-desc'
+ | 'status-true'
+ | 'status-false'
+ | 'storage-used-asc'
+ | 'storage-used-desc'
+ | 'last-save-asc'
+ | 'last-save-desc';
+
+export default function RepoList() {
+ const router = useRouter();
+ const { mutate } = useSWRConfig();
+ const [displayRepoAdd, setDisplayRepoAdd] = useState(false);
+ const [displayRepoEdit, setDisplayRepoEdit] = useState(false);
+ const [wizardEnv, setWizardEnv] = useState>();
+
+ const [sortOption, setSortOption] = useState(() => {
+ const savedSort = localStorage.getItem('repoSort');
+ return (savedSort as SortOption) || 'alias-asc';
+ });
+
+ const [searchQuery, setSearchQuery] = useState(() => {
+ const savedSearch = localStorage.getItem('repoSearch');
+ return savedSearch || '';
+ });
+
+ const toastOptions: ToastOptions = {
+ position: 'top-right',
+ autoClose: 8000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ };
+
+ useEffect(() => {
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ setDisplayRepoAdd(router.pathname === '/manage-repo/add');
+ setDisplayRepoEdit(router.pathname.startsWith('/manage-repo/edit'));
+
+ const fetchWizardEnv = async () => {
+ try {
+ const response = await fetch('/api/v1/account/wizard-env');
+ const data: WizardEnvType = await response.json();
+ setWizardEnv(data);
+ } catch (error) {
+ console.log('Fetching wizard env error');
+ }
+ };
+ fetchWizardEnv();
+ }, [router.pathname]);
+
+ const fetcher = async (url: string) => await fetch(url).then((res) => res.json());
+ const { data, error } = useSWR('/api/v1/repositories', fetcher);
+
+ if (!data) {
+ mutate('/api/v1/repositories');
+ return ;
+ }
+
+ if (error || data.status == 500) {
+ toast.error('Error loading repositories.', toastOptions);
+ return ;
+ }
+
+ const handleSortChange = (option: SortOption) => {
+ setSortOption(option);
+ localStorage.setItem('repoSort', option);
+ };
+
+ const handleSearchChange = (e: React.ChangeEvent) => {
+ const query = e.target.value;
+ setSearchQuery(query);
+ localStorage.setItem('repoSearch', query);
+ };
+
+ const getSortedRepoList = () => {
+ let repoList = [...data.repoList];
+
+ // Filter
+ if (searchQuery) {
+ repoList = repoList.filter((repo) =>
+ `${repo.alias} ${repo.comment} ${repo.repositoryName}`
+ .toLowerCase()
+ .includes(searchQuery.toLowerCase())
+ );
+ }
+
+ // Sort
+ switch (sortOption) {
+ case 'alias-asc':
+ return repoList.sort((a, b) => a.alias.localeCompare(b.alias));
+ case 'alias-desc':
+ return repoList.sort((a, b) => b.alias.localeCompare(a.alias));
+ case 'status-true':
+ return repoList.sort((a, b) => Number(b.status) - Number(a.status));
+ case 'status-false':
+ return repoList.sort((a, b) => Number(a.status) - Number(b.status));
+ case 'storage-used-asc':
+ return repoList.sort((a, b) => {
+ const aRatio = a.storageSize ? a.storageUsed / a.storageSize : 0;
+ const bRatio = b.storageSize ? b.storageUsed / b.storageSize : 0;
+ return aRatio - bRatio;
+ });
+ case 'storage-used-desc':
+ return repoList.sort((a, b) => {
+ const aRatio = a.storageSize ? a.storageUsed / a.storageSize : 0;
+ const bRatio = b.storageSize ? b.storageUsed / b.storageSize : 0;
+ return bRatio - aRatio;
+ });
+ case 'last-save-asc':
+ return repoList.sort((a, b) => {
+ const aDate = a.lastSave ? new Date(a.lastSave).getTime() : 0;
+ const bDate = b.lastSave ? new Date(b.lastSave).getTime() : 0;
+ return aDate - bDate;
+ });
+ case 'last-save-desc':
+ return repoList.sort((a, b) => {
+ const aDate = a.lastSave ? new Date(a.lastSave).getTime() : 0;
+ const bDate = b.lastSave ? new Date(b.lastSave).getTime() : 0;
+ return bDate - aDate;
+ });
+ default:
+ return repoList;
+ }
+ };
+
+ const manageRepoAddHandler = () => router.replace('/manage-repo/add');
+ const manageRepoEditHandler = (id: number) => router.replace('/manage-repo/edit/' + id);
+ const closeRepoManageBoxHandler = () => router.replace('/');
+ const displayBlur = () =>
+ displayRepoAdd || displayRepoEdit ? classes.containerBlur : classes.container;
+
+ const renderRepoList = getSortedRepoList().map((repo: Repository) => (
+ manageRepoEditHandler(repo.id)}
+ wizardEnv={wizardEnv}
+ />
+ ));
+
+ return (
+ <>
+
+
+
+
+ Add a repository
+
+
+
+
+
+
+ {searchQuery && (
+
+ handleSearchChange({
+ target: { value: '' },
+ } as React.ChangeEvent)
+ }
+ className={classes.clearButton}
+ title='Clear search'
+ >
+
+
+ )}
+
+
+
+ handleSortChange('alias-asc')}
+ title='Alias A-Z'
+ />
+ handleSortChange('alias-desc')}
+ title='Alias Z-A'
+ />
+ handleSortChange('status-true')}
+ title='Status OK โ KO'
+ />
+ handleSortChange('status-false')}
+ title='Status KO โ OK'
+ />
+ handleSortChange('last-save-desc')}
+ title='Last save (recent โ old)'
+ />
+ handleSortChange('last-save-asc')}
+ title='Last save (old โ recent)'
+ />
+ handleSortChange('storage-used-asc')}
+ title='Storage usage % low โ high'
+ />
+ handleSortChange('storage-used-desc')}
+ title='Storage usage % high โ low'
+ />
+
+
+
+
+
+
+ {displayRepoAdd && (
+
+ )}
+ {displayRepoEdit && (
+
+ )}
+ >
+ );
+}
diff --git a/Containers/RepoManage/RepoManage.js b/Containers/RepoManage/RepoManage.js
deleted file mode 100644
index 251b24d..0000000
--- a/Containers/RepoManage/RepoManage.js
+++ /dev/null
@@ -1,571 +0,0 @@
-//Lib
-import classes from './RepoManage.module.css';
-import { IconAlertCircle, IconX } from '@tabler/icons-react';
-import { useState } from 'react';
-import { useRouter } from 'next/router';
-import { toast } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-import { useForm, Controller } from 'react-hook-form';
-import { SpinnerDotted } from 'spinners-react';
-import Select from 'react-select';
-import Link from 'next/link';
-import { IconExternalLink } from '@tabler/icons-react';
-
-export default function RepoManage(props) {
- ////Var
- let targetRepo;
- const router = useRouter();
- const {
- register,
- handleSubmit,
- control,
- formState: { errors, isSubmitting, isValid },
- } = useForm({ mode: 'onChange' });
- //List of possible times for alerts
- const alertOptions = [
- { value: 0, label: 'Disabled' },
- { value: 3600, label: '1 hour' },
- { value: 21600, label: '6 hours' },
- { value: 43200, label: '12 hours' },
- { value: 90000, label: '1 day' },
- { value: 172800, label: '2 days' },
- { value: 259200, label: '3 days' },
- { value: 345600, label: '4 days' },
- { value: 432000, label: '5 days' },
- { value: 518400, label: '6 days' },
- { value: 604800, label: '7 days' },
- { value: 864000, label: '10 days' },
- { value: 1209600, label: '14 days' },
- { value: 2592000, label: '30 days' },
- ];
- const toastOptions = {
- position: 'top-right',
- autoClose: 5000,
- hideProgressBar: false,
- closeOnClick: true,
- pauseOnHover: true,
- draggable: true,
- progress: undefined,
- };
-
- ////State
- const [deleteDialog, setDeleteDialog] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
-
- ////Functions
- //router.query.slug is undefined for few milliseconds on first render for a direct URL access (https://github.com/vercel/next.js/discussions/11484).
- //If I call repoManage with edit mode (props), i'm firstly waiting that router.query.slug being available before rendering.
- if (!router.query.slug && props.mode == 'edit') {
- return (
-
- );
- } else if (props.mode == 'edit') {
- for (let element in props.repoList) {
- if (props.repoList[element].id == router.query.slug) {
- targetRepo = props.repoList[element];
- }
- }
- //If the ID does not exist > 404
- if (!targetRepo) {
- router.push('/404');
- return null;
- }
- }
-
- //Delete a repo
- const deleteHandler = async () => {
- //API Call for delete
- fetch('/api/repo/id/' + router.query.slug + '/delete', {
- method: 'DELETE',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify({ toDelete: true }),
- })
- .then((response) => {
- if (response.ok) {
- toast.success(
- '๐ The repository #' +
- router.query.slug +
- ' has been successfully deleted',
- toastOptions
- );
- router.replace('/');
- } else {
- toast.error('An error has occurred', toastOptions);
- router.replace('/');
- console.log('Fail to delete');
- }
- })
- .catch((error) => {
- toast.error('An error has occurred', toastOptions);
- router.replace('/');
- console.log(error);
- });
- };
-
- //Verify that the SSH key is unique
- const isSSHKeyUnique = async (sshPublicKey) => {
- let isUnique = true;
-
- // Extract the first two columns of the SSH key in the form
- const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' ');
-
- await fetch('/api/repo', { method: 'GET' })
- .then((response) => response.json())
- .then((data) => {
- for (let element in data.repoList) {
- // Extract the first two columns of the SSH key in the repoList
- const repoPublicKeyPrefix = data.repoList[
- element
- ].sshPublicKey
- .split(' ')
- .slice(0, 2)
- .join(' ');
-
- if (
- repoPublicKeyPrefix === publicKeyPrefix && // Compare the first two columns of the SSH key
- (!targetRepo ||
- data.repoList[element].id != targetRepo.id)
- ) {
- toast.error(
- 'The SSH key is already used in repository #' +
- data.repoList[element].id +
- '. Please use another key or delete the key from the other repository.',
- toastOptions
- );
- isUnique = false;
- break;
- }
- }
- })
- .catch((error) => {
- console.log(error);
- toast.error('An error has occurred', toastOptions);
- isUnique = false;
- });
- return isUnique;
- };
-
- //Form submit Handler for ADD or EDIT a repo
- const formSubmitHandler = async (dataForm) => {
- //Loading button on submit to avoid multiple send.
- setIsLoading(true);
- //Verify that the SSH key is unique
- if (!(await isSSHKeyUnique(dataForm.sshkey))) {
- setIsLoading(false);
- return;
- }
- //ADD a repo
- if (props.mode == 'add') {
- const newRepo = {
- alias: dataForm.alias,
- size: dataForm.size,
- sshPublicKey: dataForm.sshkey,
- comment: dataForm.comment,
- alert: dataForm.alert.value,
- lanCommand: dataForm.lanCommand,
- appendOnlyMode: dataForm.appendOnlyMode,
- };
- //POST API to send new repo
- await fetch('/api/repo/add', {
- method: 'POST',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify(newRepo),
- })
- .then(async (response) => {
- if (response.ok) {
- toast.success(
- 'New repository added ! ๐ฅณ',
- toastOptions
- );
- router.replace('/');
- } else {
- const errorMessage = await response.json();
- toast.error(
- `An error has occurred : ${errorMessage.message}`,
- toastOptions
- );
- router.replace('/');
- console.log(`Fail to ${props.mode}`);
- }
- })
- .catch((error) => {
- toast.error('An error has occurred', toastOptions);
- router.replace('/');
- console.log(error);
- });
- //EDIT a repo
- } else if (props.mode == 'edit') {
- const dataEdited = {
- alias: dataForm.alias,
- size: dataForm.size,
- sshPublicKey: dataForm.sshkey,
- comment: dataForm.comment,
- alert: dataForm.alert.value,
- lanCommand: dataForm.lanCommand,
- appendOnlyMode: dataForm.appendOnlyMode,
- };
- await fetch('/api/repo/id/' + router.query.slug + '/edit', {
- method: 'PUT',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify(dataEdited),
- })
- .then(async (response) => {
- if (response.ok) {
- toast.success(
- 'The repository #' +
- targetRepo.id +
- ' has been successfully edited !',
- toastOptions
- );
- router.replace('/');
- } else {
- const errorMessage = await response.json();
- toast.error(
- `An error has occurred : ${errorMessage.message}`,
- toastOptions
- );
- router.replace('/');
- console.log(`Fail to ${props.mode}`);
- }
- })
- .catch((error) => {
- toast.error('An error has occurred', toastOptions);
- router.replace('/');
- console.log(error);
- });
- }
- };
-
- return (
- <>
-
-
-
-
-
- {deleteDialog ? (
-
-
-
-
- Delete the repository{' '}
-
- #{targetRepo.id}
- {' '}
- ?
-
-
-
-
- You are about to permanently delete the
- repository #{targetRepo.id} and all the
- backups it contains.
-
-
- The data will not be recoverable and it will not
- be possible to go back.
-
-
-
- {isLoading ? (
-
- ) : (
- <>
-
- Cancel
-
- {
- deleteHandler();
- setIsLoading(true);
- }}
- className={classes.deleteButton}
- >
- Yes, delete it !
-
- >
- )}
-
-
- ) : (
-
- {props.mode == 'edit' && (
-
- Edit the repository{' '}
-
- #{targetRepo.id}
-
-
- )}
- {props.mode == 'add' &&
Add a repository }
-
- {props.mode == 'edit' ? (
-
setDeleteDialog(true)}
- >
- Delete this repository
-
- ) : null}
-
- )}
-
- >
- );
-}
diff --git a/Containers/RepoManage/RepoManage.module.css b/Containers/RepoManage/RepoManage.module.css
index aa603c5..8c55d5c 100644
--- a/Containers/RepoManage/RepoManage.module.css
+++ b/Containers/RepoManage/RepoManage.module.css
@@ -1,268 +1,296 @@
.modaleWrapper {
- position: fixed;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- width: 100%;
- height: 100%;
- margin: 50px 0px 0px 70px;
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: 100%;
+ height: 100%;
+ margin: 50px 0px 0px 70px;
}
.modale {
- position: fixed;
- top: 10%;
- width: 1000px;
- height: auto;
- max-width: 75%;
- max-height: 85%;
- background: #fff;
- padding: 20px 20px 20px;
- overflow: auto;
- border-radius: 10px;
- box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.4);
- margin: 0 auto;
- animation: append-animate 0.3s linear;
+ position: fixed;
+ top: 10%;
+ width: 800px;
+ height: auto;
+ max-width: 75%;
+ max-height: 85%;
+ background: #fff;
+ padding: 20px 20px 20px;
+ overflow: auto;
+ border-radius: 10px;
+ box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.4);
+ margin: 0 auto;
+ animation: append-animate 0.3s linear;
+}
+
+.modale h2 {
+ margin-top: 0;
+ color: #374151;
}
@keyframes append-animate {
- from {
- transform: scale(0);
- opacity: 0;
- }
- to {
- transform: scale(1);
- opacity: 1;
- }
+ from {
+ transform: scale(0);
+ opacity: 0;
+ }
+ to {
+ transform: scale(1);
+ opacity: 1;
+ }
}
.close {
- cursor: pointer;
- margin-left: 95%;
- color: #494b7a;
+ cursor: pointer;
+ margin-left: 95%;
+ color: #494b7a;
}
.close :hover {
- color: #aa60ff;
+ color: #aa60ff;
}
.repoManageForm {
- margin: auto;
- width: 80%;
- padding: 15px 30px 30px 30px;
- border-radius: 5px;
- text-align: left;
+ margin: auto;
+ width: 100%;
+ max-width: 600px;
+ border-radius: 8px;
+ background-color: #ffffff;
+ font-family: Inter, sans-serif;
+ color: #1f2937;
}
.formWrapper {
- text-align: center;
- margin: auto;
- width: 100%;
- height: auto;
- color: #494b7a;
+ text-align: center;
+ width: 100%;
+ margin: 0 auto;
+ color: inherit;
}
.repoManageForm label {
- display: block;
- margin-bottom: 8px;
- text-align: center;
- margin-top: 20px;
- color: #494b7a;
+ display: block;
+ margin-top: 0.9rem;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+ font-size: 0.95rem;
+ color: #374151;
+ text-align: left;
}
.repoManageForm input,
.repoManageForm textarea,
.repoManageForm select {
- border: 1px solid #6d4aff21;
- font-size: 16px;
- height: auto;
- margin: 0;
- margin-bottom: 0px;
- outline: 0;
- padding: 15px;
- width: 100%;
- background-color: #f5f5f5;
- border-radius: 5px;
- /* color: #1b1340; */
- color: #494b7a;
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ background-color: #f9fafb;
+ color: #111827;
+ font-family: Inter;
}
.repoManageForm textarea {
- resize: vertical;
+ resize: vertical;
+ min-height: 80px;
}
-.repoManageForm textarea:focus,
.repoManageForm input:focus,
+.repoManageForm textarea:focus,
.repoManageForm select:focus {
- outline: 1px solid #6d4aff;
- box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
+ border-color: #6d4aff;
+ background-color: #ffffff;
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(109, 74, 255, 0.3);
}
.repoManageForm .invalid {
- background: #f3c7c7;
- border: 1px solid #e45454;
- outline: 1px solid #ff4a4a;
+ background-color: #fef2f2;
+ border-color: #ef4444;
}
.repoManageForm .invalid:focus {
- background: #f3c7c7;
- border: 1px solid #e45454;
- outline: 1px solid #ff4a4a;
- box-shadow: 0 0 10px 3px rgba(255, 74, 74, 0.605);
+ box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
}
.repoManageForm button {
- display: block;
- margin: 15px auto;
+ display: block;
+ margin: 2rem auto 0 auto;
+ background-color: #6d4aff;
+ color: #ffffff;
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 6px;
+ font-size: 1rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
}
+
.repoManageForm button:hover {
- display: block;
- margin: 15px auto;
+ background-color: #5c3dff;
}
.errorMessage {
- color: red;
- display: block;
- margin-top: 3px;
+ color: #dc2626;
+ font-size: 0.875rem;
+ margin-top: 0.3rem;
}
.optionCommandWrapper {
- display: flex;
- margin-top: 20px;
- color: #494b7a;
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ margin-top: 1.5rem;
+ font-size: 0.95rem;
+ color: #374151;
}
.optionCommandWrapper label {
- margin: 0;
+ margin: 0;
}
.optionCommandWrapper input[type='checkbox'] {
- width: auto;
- margin-right: 8px;
- cursor: pointer;
- accent-color: #6d4aff;
+ width: 18px;
+ height: 18px;
+ accent-color: #6d4aff;
+ cursor: pointer;
}
+
.optionCommandWrapper input[type='checkbox']:focus {
- outline: 0;
- box-shadow: none;
- accent-color: #6d4aff;
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(109, 74, 255, 0.4);
+}
+
+.selectAlert {
+ max-width: 160px;
+}
+.selectAlertWrapper label {
+ margin: 0;
+}
+
+.selectAlertWrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ margin-top: 1.5rem;
+ gap: 0.5rem;
+ font-size: 0.95rem;
+ color: #374151;
}
/* DELETE DIALOG */
.deleteDialogWrapper {
- text-align: center;
- margin: auto;
- width: 80%;
- height: 100%;
- max-height: 590px;
- color: #111827;
- display: flex;
- flex-direction: column;
- justify-content: center;
+ text-align: center;
+ margin: auto;
+ width: 80%;
+ height: 100%;
+ max-height: 590px;
+ color: #111827;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
}
.deleteDialogMessage {
- background-color: #ea1313;
- color: #fff;
- font-weight: 500;
- border-radius: 5px;
- padding: 15px;
- margin-top: 15px;
- margin-bottom: 15px;
- font-size: 1.1em;
+ background-color: #ea1313;
+ color: #fff;
+ font-weight: 500;
+ border-radius: 5px;
+ padding: 15px;
+ margin-top: 15px;
+ margin-bottom: 15px;
+ font-size: 1.1em;
}
.cancelButton {
- border: 0;
- padding: 10px 15px;
- background-color: #c1c1c1;
- color: white;
- margin: 5px;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-weight: bold;
- font-size: 1em;
+ border: 0;
+ padding: 10px 15px;
+ background-color: #c1c1c1;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
}
.cancelButton:hover {
- border: 0;
- padding: 10px 15px;
- background-color: #9a9a9a;
- color: white;
- margin: 5px;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-weight: bold;
- font-size: 1em;
+ border: 0;
+ padding: 10px 15px;
+ background-color: #9a9a9a;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
}
.cancelButton:active {
- border: 0;
- padding: 10px 15px;
- background-color: #9a9a9a;
- color: white;
- margin: 5px;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-weight: bold;
- font-size: 1em;
- transform: scale(0.9);
+ border: 0;
+ padding: 10px 15px;
+ background-color: #9a9a9a;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
+ transform: scale(0.9);
}
.deleteButton {
- border: 0;
- padding: 10px 15px;
- background-color: #ff0000;
- color: white;
- margin: 5px;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-weight: bold;
- font-size: 1em;
+ border: 0;
+ padding: 10px 15px;
+ background-color: #ff0000;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
}
.deleteButton:hover {
- border: 0;
- padding: 10px 15px;
- background-color: #ff4b4b;
- color: white;
- margin: 5px;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-weight: bold;
- font-size: 1em;
+ border: 0;
+ padding: 10px 15px;
+ background-color: #ff4b4b;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
}
.deleteButton:active {
- border: 0;
- padding: 10px 15px;
- background-color: #ff4b4b;
- color: white;
- margin: 5px;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-weight: bold;
- font-size: 1em;
- transform: scale(0.9);
+ border: 0;
+ padding: 10px 15px;
+ background-color: #ff4b4b;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
+ transform: scale(0.9);
}
.littleDeleteButton {
- border: none;
- font-weight: 300;
- color: red;
- text-decoration: underline;
- background: none;
- cursor: pointer;
-}
-
-.selectAlert {
- margin: auto auto 35px auto;
- max-width: 160px;
+ margin-top: 10px;
+ border: none;
+ font-weight: 300;
+ color: red;
+ text-decoration: underline;
+ background: none;
+ cursor: pointer;
}
diff --git a/Containers/RepoManage/RepoManage.tsx b/Containers/RepoManage/RepoManage.tsx
new file mode 100644
index 0000000..b37f0cd
--- /dev/null
+++ b/Containers/RepoManage/RepoManage.tsx
@@ -0,0 +1,496 @@
+import { IconAlertCircle, IconExternalLink, IconX } from '@tabler/icons-react';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { useState } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import Select from 'react-select';
+import { toast, ToastOptions } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { useLoader } from '~/contexts/LoaderContext';
+import { alertOptions, Optional, Repository } from '~/types';
+import classes from './RepoManage.module.css';
+
+type RepoManageProps = {
+ mode: 'add' | 'edit';
+ repoList: Optional>;
+ closeHandler: () => void;
+};
+
+type DataForm = {
+ alias: string;
+ storageSize: string;
+ sshkey: string;
+ comment: string;
+ alert: { value: Optional; label: string };
+ lanCommand: boolean;
+ appendOnlyMode: boolean;
+};
+
+export default function RepoManage(props: RepoManageProps) {
+ const router = useRouter();
+ const targetRepo =
+ props.mode === 'edit' && router.query.slug
+ ? props.repoList?.find((repo) => repo.id.toString() === router.query.slug)
+ : undefined;
+
+ const {
+ register,
+ handleSubmit,
+ control,
+ formState: { errors, isSubmitting, isValid },
+ } = useForm({ mode: 'onChange' });
+
+ const toastOptions: ToastOptions = {
+ position: 'top-right',
+ autoClose: 5000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ };
+
+ const [deleteDialog, setDeleteDialog] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const { start, stop } = useLoader();
+
+ //router.query.slug is undefined for few milliseconds on first render for a direct URL access (https://github.com/vercel/next.js/discussions/11484).
+ //If I call repoManage with edit mode (props), i'm firstly waiting that router.query.slug being available before rendering.
+ if (props.mode === 'edit') {
+ if (!router.query.slug) {
+ start();
+ return;
+ } else if (!targetRepo) {
+ stop();
+ router.push('/404');
+ }
+ }
+
+ //Delete a repo
+ const deleteHandler = async (repositoryName?: string) => {
+ start();
+ if (!repositoryName) {
+ stop();
+ toast.error('Repository name not found', toastOptions);
+ router.replace('/');
+ return;
+ }
+ //API Call for delete
+ await fetch('/api/v1/repositories/' + repositoryName, {
+ method: 'DELETE',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ })
+ .then(async (response) => {
+ if (response.ok) {
+ toast.success(
+ '๐ The repository ' + repositoryName + ' has been successfully deleted',
+ toastOptions
+ );
+ router.replace('/');
+ } else {
+ if (response.status == 403) {
+ toast.warning(
+ '๐ The server is currently protected against repository deletion.',
+ toastOptions
+ );
+ setIsLoading(false);
+ router.replace('/');
+ } else {
+ const errorMessage = await response.json();
+ toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
+ router.replace('/');
+ console.log('Fail to delete');
+ }
+ }
+ })
+ .catch((error) => {
+ toast.error('An error has occurred', toastOptions);
+ router.replace('/');
+ console.log(error);
+ })
+ .finally(() => {
+ stop();
+ });
+ };
+
+ const isSSHKeyUnique = async (sshPublicKey: string): Promise => {
+ try {
+ // Extract the first two columns of the SSH key in the form
+ const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' ');
+
+ const response = await fetch('/api/v1/repositories', { method: 'GET' });
+ const data: { repoList: Repository[] } = await response.json();
+
+ const conflictingRepo = data.repoList.find((repo: { sshPublicKey: string; id: number }) => {
+ const repoPublicKeyPrefix = repo.sshPublicKey.split(' ').slice(0, 2).join(' ');
+ return (
+ repoPublicKeyPrefix === publicKeyPrefix && (!targetRepo || repo.id !== targetRepo.id)
+ );
+ });
+
+ if (conflictingRepo) {
+ toast.error(
+ `The SSH key is already used in repository ${conflictingRepo.repositoryName}. Please use another key or delete the key from the other repository.`,
+ toastOptions
+ );
+ return false;
+ }
+
+ return true;
+ } catch (error) {
+ console.log(error);
+ toast.error('An error has occurred', toastOptions);
+ return false;
+ }
+ };
+
+ //Form submit Handler for ADD or EDIT a repo
+ const formSubmitHandler = async (dataForm: DataForm) => {
+ setIsLoading(true);
+ start();
+
+ // Clean SSH key by removing leading/trailing whitespace and line breaks
+ const cleanedSSHKey = dataForm.sshkey.trim();
+
+ //Verify that the SSH key is unique
+ if (!(await isSSHKeyUnique(cleanedSSHKey))) {
+ stop();
+ setIsLoading(false);
+ return;
+ }
+ //ADD a repo
+ if (props.mode == 'add') {
+ const newRepo = {
+ alias: dataForm.alias,
+ storageSize: parseInt(dataForm.storageSize),
+ sshPublicKey: cleanedSSHKey,
+ comment: dataForm.comment,
+ alert: dataForm.alert.value,
+ lanCommand: dataForm.lanCommand,
+ appendOnlyMode: dataForm.appendOnlyMode,
+ };
+ //POST API to send new repo
+ await fetch('/api/v1/repositories', {
+ method: 'POST',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify(newRepo),
+ })
+ .then(async (response) => {
+ if (response.ok) {
+ toast.success('New repository added ! ๐ฅณ', toastOptions);
+ router.replace('/');
+ } else {
+ const errorMessage = await response.json();
+ toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
+ router.replace('/');
+ console.log(`Fail to ${props.mode}`);
+ }
+ })
+ .catch((error) => {
+ toast.error('An error has occurred', toastOptions);
+ router.replace('/');
+ console.log(error);
+ })
+ .finally(() => {
+ stop();
+ setIsLoading(false);
+ });
+ //EDIT a repo
+ } else if (props.mode == 'edit') {
+ const dataEdited = {
+ alias: dataForm.alias,
+ storageSize: parseInt(dataForm.storageSize),
+ sshPublicKey: cleanedSSHKey,
+ comment: dataForm.comment,
+ alert: dataForm.alert.value,
+ lanCommand: dataForm.lanCommand,
+ appendOnlyMode: dataForm.appendOnlyMode,
+ };
+ await fetch('/api/v1/repositories/' + targetRepo?.repositoryName, {
+ method: 'PATCH',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify(dataEdited),
+ })
+ .then(async (response) => {
+ if (response.ok) {
+ toast.success(
+ 'The repository ' + targetRepo?.repositoryName + ' has been successfully edited !',
+ toastOptions
+ );
+ router.replace('/');
+ } else {
+ const errorMessage = await response.json();
+ toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
+ router.replace('/');
+ console.log(`Fail to ${props.mode}`);
+ }
+ })
+ .catch((error) => {
+ toast.error('An error has occurred', toastOptions);
+ router.replace('/');
+ console.log(error);
+ })
+ .finally(() => {
+ stop();
+ setIsLoading(false);
+ });
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {deleteDialog ? (
+
+
+
+
+ Delete the repository{' '}
+
+ {targetRepo?.repositoryName}
+ {' '}
+ ?
+
+
+
+
+ You are about to permanently delete the repository{' '}
+ {targetRepo?.repositoryName} and all the backups it contains.
+
+
The data will not be recoverable and it will not be possible to go back.
+
+
+ <>
+
+ Cancel
+
+ {
+ deleteHandler(targetRepo?.repositoryName);
+ setIsLoading(true);
+ }}
+ className={classes.deleteButton}
+ >
+ Yes, delete it !
+
+ >
+
+
+ ) : (
+
+ {props.mode == 'edit' && (
+
+ Edit the repository{' '}
+
+ {targetRepo?.repositoryName}
+
+
+ )}
+ {props.mode == 'add' &&
Add a repository }
+
+ {/* ALIAS */}
+ Alias
+
+ {errors.alias && {errors.alias.message} }
+ {/* SSH KEY */}
+ SSH public key
+ {
+ const trimmedValue = value.trim();
+ const pattern =
+ /^(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?$/;
+ return (
+ pattern.test(trimmedValue) ||
+ 'Invalid public key. The key needs to be in OpenSSH format (rsa, ed25519, ed25519-sk)'
+ );
+ },
+ })}
+ />
+ {errors.sshkey && (
+ {errors.sshkey.message}
+ )}
+ {/* storageSize */}
+ Storage Size (GB)
+
+ {errors.storageSize && (
+ {errors.storageSize.message}
+ )}
+ {/* COMMENT */}
+ Comment
+
+ {errors.comment && (
+ {errors.comment.message}
+ )}
+ {/* LAN COMMAND GENERATION */}
+
+
+ Generates commands for use over LAN
+
+
+
+
+ {/* APPEND-ONLY MODE */}
+
+
+ Enable append-only mode
+
+
+
+
+ {/* ALERT */}
+
+
Alert if there is no backup since :
+
+ x.value === targetRepo?.alert) || {
+ value: targetRepo?.alert,
+ label: `Custom value (${targetRepo?.alert} seconds)`,
+ }
+ : alertOptions[4]
+ }
+ control={control}
+ render={({ field: { onChange, value } }) => (
+ ({
+ ...base,
+ minHeight: '35px',
+ height: '35px',
+ }),
+ valueContainer: (base) => ({
+ ...base,
+ height: '35px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexDirection: 'column',
+ padding: '0 8px',
+ }),
+ input: (base) => ({
+ ...base,
+ margin: 0,
+ }),
+ indicatorsContainer: (base) => ({
+ ...base,
+ height: '35px',
+ }),
+ }}
+ theme={(theme) => ({
+ ...theme,
+ borderRadius: 5,
+ colors: {
+ ...theme.colors,
+ primary25: '#c3b6fa',
+ primary: '#6d4aff',
+ },
+ })}
+ />
+ )}
+ />
+
+
+
+
+ {props.mode == 'edit' && 'Save'}
+ {props.mode == 'add' && 'Add repository'}
+
+
+ {props.mode == 'edit' ? (
+
setDeleteDialog(true)}>
+ Delete this repository
+
+ ) : null}
+
+ )}
+
+ >
+ );
+}
diff --git a/Containers/SetupWizard/SetupWizard.js b/Containers/SetupWizard/SetupWizard.js
deleted file mode 100644
index 78e426d..0000000
--- a/Containers/SetupWizard/SetupWizard.js
+++ /dev/null
@@ -1,159 +0,0 @@
-//Lib
-import React from 'react';
-import classes from './SetupWizard.module.css';
-import { useState, useEffect } from 'react';
-import { useRouter } from 'next/router';
-import Select from 'react-select';
-
-//Components
-import WizardStepBar from '../../Components/WizardSteps/WizardStepBar/WizardStepBar';
-import WizardStep1 from '../../Components/WizardSteps/WizardStep1/WizardStep1';
-import WizardStep2 from '../../Components/WizardSteps/WizardStep2/WizardStep2';
-import WizardStep3 from '../../Components/WizardSteps/WizardStep3/WizardStep3';
-import WizardStep4 from '../../Components/WizardSteps/WizardStep4/WizardStep4';
-
-function SetupWizard(props) {
- ////Var
- const router = useRouter();
-
- ////States
- const [list, setList] = useState([]);
- const [listIsLoading, setListIsLoading] = useState(true);
- const [step, setStep] = useState();
- const [wizardEnv, setWizardEnv] = useState({});
- const [selectedOption, setSelectedOption] = useState({
- id: '#id',
- repository: 'repo',
- });
-
- ////LifeCycle
- //ComponentDidMount
- useEffect(() => {
- //retrieve the repository list
- const repoList = async () => {
- try {
- const response = await fetch('/api/repo', {
- method: 'GET',
- headers: {
- 'Content-type': 'application/json',
- },
- });
- setList((await response.json()).repoList);
- setListIsLoading(false);
- } catch (error) {
- console.log('Fetching datas error');
- }
- };
- repoList();
- //Fetch wizardEnv to hydrate Wizard' steps
- const fetchWizardEnv = async () => {
- try {
- const response = await fetch('/api/account/getWizardEnv', {
- method: 'GET',
- headers: {
- 'Content-type': 'application/json',
- },
- });
- setWizardEnv((await response.json()).wizardEnv);
- } catch (error) {
- console.log('Fetching datas error');
- }
- };
- fetchWizardEnv();
- }, []);
- //Component did update
- useEffect(() => {
- //Go to the step in the URL param when URL change
- setStep(props.step);
- }, [props.step]);
-
- ////Functions
-
- //Options for react-select
- const options = list.map((repo) => ({
- label: `${repo.alias} - #${repo.id}`,
- value: `${repo.alias} - #${repo.id}`,
- id: repo.id,
- repositoryName: repo.repositoryName,
- lanCommand: repo.lanCommand,
- }));
-
- //Step button (free selection of user)
- const changeStepHandler = (x) => router.push('/setup-wizard/' + x);
-
- //Next Step button
- const nextStepHandler = () => {
- if (step < 4) {
- router.push('/setup-wizard/' + `${Number(step) + 1}`);
- }
- };
-
- //Previous Step button
- const previousStepHandler = () => {
- if (step > 1) {
- router.push('/setup-wizard/' + `${Number(step) - 1}`);
- }
- };
-
- //Change Step with State
- const wizardStep = (step) => {
- if (step == 1) {
- return ;
- } else if (step == 2) {
- return (
-
- );
- } else if (step == 3) {
- return (
-
- );
- } else {
- return (
-
- );
- }
- };
-
- return (
-
-
changeStepHandler(x)}
- step={step}
- nextStepHandler={() => nextStepHandler()}
- previousStepHandler={() => previousStepHandler()}
- />
-
- ({
- ...theme,
- borderRadius: '5px',
- colors: {
- ...theme.colors,
- primary25: '#c3b6fa',
- primary: '#6d4aff',
- },
- })}
- />
-
-
- {wizardStep(step)}
-
- );
-}
-
-export default SetupWizard;
diff --git a/Containers/SetupWizard/SetupWizard.module.css b/Containers/SetupWizard/SetupWizard.module.css
index 013e72b..7a6f86e 100644
--- a/Containers/SetupWizard/SetupWizard.module.css
+++ b/Containers/SetupWizard/SetupWizard.module.css
@@ -1,28 +1,28 @@
.container {
- display: flex;
- flex-direction: column;
- justify-content: center;
- color: #494b7a;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ color: #494b7a;
}
.container a {
- text-decoration: none;
+ text-decoration: none;
}
.wizardStepTransition-enter {
- opacity: 0;
+ opacity: 0;
}
.wizardStepTransition-enter-active {
- opacity: 1;
- transition: opacity 200ms;
+ opacity: 1;
+ transition: opacity 200ms;
}
.wizardStepTransition-exit {
- opacity: 1;
+ opacity: 1;
}
.wizardStepTransition-exit-active {
- opacity: 0;
- transition: opacity 200ms;
+ opacity: 0;
+ transition: opacity 200ms;
}
.selectRepo {
- margin: 30px auto auto auto;
- width: 300px;
+ margin: 30px auto auto auto;
+ width: 300px;
}
diff --git a/Containers/SetupWizard/SetupWizard.tsx b/Containers/SetupWizard/SetupWizard.tsx
new file mode 100644
index 0000000..f6fb8e6
--- /dev/null
+++ b/Containers/SetupWizard/SetupWizard.tsx
@@ -0,0 +1,170 @@
+import { useRouter } from 'next/router';
+import { useEffect, useMemo, useState } from 'react';
+import Select, { SingleValue } from 'react-select';
+import classes from './SetupWizard.module.css';
+import { Optional, SelectedRepoWizard, Repository, WizardEnvType } from '~/types';
+
+//Components
+import WizardStep1 from '../../Components/WizardSteps/WizardStep1/WizardStep1';
+import WizardStep2 from '../../Components/WizardSteps/WizardStep2/WizardStep2';
+import WizardStep3 from '../../Components/WizardSteps/WizardStep3/WizardStep3';
+import WizardStep4 from '../../Components/WizardSteps/WizardStep4/WizardStep4';
+import WizardStepBar from '../../Components/WizardSteps/WizardStepBar/WizardStepBar';
+
+type SetupWizardProps = {
+ step?: number;
+};
+
+function SetupWizard(props: SetupWizardProps) {
+ const router = useRouter();
+
+ const [repoList, setRepoList] = useState>>();
+ const [repoListIsLoading, setRepoListIsLoading] = useState(true);
+ const [step, setStep] = useState(1);
+ const [wizardEnv, setWizardEnv] = useState>();
+ const [selectedItem, setSelectedItem] = useState>();
+
+ ////LifeCycle
+ //ComponentDidMount
+ useEffect(() => {
+ //retrieve the repository list
+ const fetchRepoList = async () => {
+ try {
+ const response = await fetch('/api/v1/repositories', {
+ method: 'GET',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ });
+ const data = await response.json();
+ const repos = data.repoList;
+ setRepoList(repos);
+ setRepoListIsLoading(false);
+
+ // Auto-select first repository if available
+ if (repos && repos.length > 0) {
+ setSelectedItem({
+ label: `${repos[0].alias} - ${repos[0].repositoryName}`,
+ value: `${repos[0].alias} - ${repos[0].repositoryName}`,
+ id: repos[0].id.toString(),
+ repositoryName: repos[0].repositoryName,
+ lanCommand: repos[0].lanCommand ? repos[0].lanCommand : false,
+ });
+ }
+ } catch (error) {
+ console.log('Fetching datas error');
+ }
+ };
+ fetchRepoList();
+ //Fetch wizardEnv to hydrate Wizard' steps
+ const fetchWizardEnv = async () => {
+ try {
+ const response = await fetch('/api/v1/account/wizard-env', {
+ method: 'GET',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ });
+ const data: WizardEnvType = await response.json();
+ setWizardEnv(data);
+ } catch (error) {
+ console.log('Fetching datas error');
+ }
+ };
+ fetchWizardEnv();
+ }, []);
+ //Component did update
+ useEffect(() => {
+ //Go to the step in the URL param when URL change
+ if (props.step) {
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ setStep(props.step);
+ }
+ }, [props.step]);
+
+ //Options for react-select
+ const options: Optional> = useMemo(
+ () =>
+ repoList?.map((repo) => ({
+ label: `${repo.alias} - ${repo.repositoryName}`,
+ value: `${repo.alias} - ${repo.repositoryName}`,
+ id: repo.id.toString(),
+ repositoryName: repo.repositoryName,
+ lanCommand: repo.lanCommand ? repo.lanCommand : false,
+ })),
+ [repoList]
+ );
+
+ //Step button (free selection of user)
+ const changeStepHandler = (x: number) => router.push('/setup-wizard/' + x.toString());
+
+ //Next Step button
+ const nextStepHandler = () => {
+ if (step && step < 4) {
+ router.push('/setup-wizard/' + `${step + 1}`);
+ }
+ };
+
+ //Previous Step button
+ const previousStepHandler = () => {
+ if (step && step > 1) {
+ router.push('/setup-wizard/' + `${step - 1}`);
+ }
+ };
+
+ const onChangeSelect = (option: SingleValue) => {
+ if (option) {
+ setSelectedItem(option);
+ } else {
+ setSelectedItem(undefined);
+ }
+ };
+
+ //Change Step with State
+ const wizardStep = (step?: number) => {
+ if (!step || step === 1) {
+ return ;
+ } else if (step === 2) {
+ return ;
+ } else if (step === 3) {
+ return ;
+ } else {
+ return ;
+ }
+ };
+
+ return (
+
+
changeStepHandler(x)}
+ step={step}
+ nextStepHandler={() => nextStepHandler()}
+ previousStepHandler={() => previousStepHandler()}
+ />
+
+ onChangeSelect(item)}
+ isLoading={repoListIsLoading}
+ isDisabled={repoListIsLoading}
+ options={options}
+ isSearchable
+ value={selectedItem}
+ placeholder='Select your repository...'
+ theme={(theme) => ({
+ ...theme,
+ borderRadius: 5,
+ colors: {
+ ...theme.colors,
+ primary25: '#c3b6fa',
+ primary: '#6d4aff',
+ },
+ })}
+ />
+
+
+ {wizardStep(step)}
+
+ );
+}
+
+export default SetupWizard;
diff --git a/Containers/UserSettings/AppriseAlertSettings/AppriseAlertSettings.js b/Containers/UserSettings/AppriseAlertSettings/AppriseAlertSettings.js
deleted file mode 100644
index f27eea4..0000000
--- a/Containers/UserSettings/AppriseAlertSettings/AppriseAlertSettings.js
+++ /dev/null
@@ -1,220 +0,0 @@
-//Lib
-import { useEffect } from 'react';
-import { toast } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-import classes from '../UserSettings.module.css';
-import { useState } from 'react';
-import { SpinnerCircularFixed } from 'spinners-react';
-import { IconExternalLink } from '@tabler/icons-react';
-import Link from 'next/link';
-
-//Components
-import Error from '../../../Components/UI/Error/Error';
-import Switch from '../../../Components/UI/Switch/Switch';
-import AppriseURLs from './AppriseURLs/AppriseURLs';
-import AppriseMode from './AppriseMode/AppriseMode';
-
-export default function AppriseAlertSettings() {
- //Var
- const toastOptions = {
- position: 'top-right',
- autoClose: 5000,
- hideProgressBar: false,
- closeOnClick: true,
- pauseOnHover: true,
- draggable: true,
- progress: undefined,
- //Callback > re-enabled button after notification.
- onClose: () => setDisabled(false),
- };
-
- ////State
- const [checkIsLoading, setCheckIsLoading] = useState(true);
- const [error, setError] = useState();
- const [disabled, setDisabled] = useState(false);
- const [checked, setChecked] = useState();
- const [testIsLoading, setTestIsLoading] = useState(false);
- const [info, setInfo] = useState(false);
-
- ////LifeCycle
- //Component did mount
- useEffect(() => {
- //Initial fetch to get the status of Apprise Alert
- const getAppriseAlert = async () => {
- try {
- const response = await fetch('/api/account/getAppriseAlert', {
- method: 'GET',
- headers: {
- 'Content-type': 'application/json',
- },
- });
- setChecked((await response.json()).appriseAlert);
- setCheckIsLoading(false);
- } catch (error) {
- setError(
- 'Fetching apprise alert setting failed. Contact your administrator.'
- );
- console.log('Fetching apprise alert setting failed.');
- setCheckIsLoading(false);
- }
- };
- getAppriseAlert();
- }, []);
-
- ////Functions
- //Switch to enable/disable Apprise notifications
- const onChangeSwitchHandler = async (data) => {
- //Remove old error
- setError();
- //Disabled button
- setDisabled(true);
- await fetch('/api/account/updateAppriseAlert', {
- method: 'PUT',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify(data),
- })
- .then((response) => {
- console.log(response);
- if (response.ok) {
- if (data.appriseAlert) {
- setChecked(!checked);
- toast.success(
- 'Apprise notifications enabled.',
- toastOptions
- );
- } else {
- setChecked(!checked);
- toast.success(
- 'Apprise notifications disabled.',
- toastOptions
- );
- }
- } else {
- setError('Update apprise alert setting failed.');
- setTimeout(() => {
- setError();
- setDisabled(false);
- }, 4000);
- }
- })
- .catch((error) => {
- console.log(error);
- setError('Update Apprise failed. Contact your administrator.');
- setTimeout(() => {
- setError();
- setDisabled(false);
- }, 4000);
- });
- };
-
- //Send Apprise test notification to services
- const onSendTestAppriseHandler = async () => {
- //Loading
- setTestIsLoading(true);
- //Remove old error
- setError();
- try {
- const response = await fetch('/api/account/sendTestApprise', {
- method: 'POST',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify({ sendTestApprise: true }),
- });
- const result = await response.json();
- if (!response.ok) {
- setTestIsLoading(false);
- setError(result.message);
- } else {
- setTestIsLoading(false);
- setInfo(true);
- setTimeout(() => {
- setInfo(false);
- }, 4000);
- }
- } catch (error) {
- setTestIsLoading(false);
- console.log(error);
- setError('Send notification failed. Contact your administrator.');
- setTimeout(() => {
- setError();
- }, 4000);
- }
- };
-
- return (
- <>
- {/* APPRISE ALERT */}
-
-
-
Apprise alert
-
-
-
-
-
-
- {/* NOTIFY SWITCH */}
- {checkIsLoading ? (
-
- ) : (
-
- onChangeSwitchHandler({ appriseAlert: e })
- }
- />
- )}
- {/* APPRISE SERVICES URLS */}
-
- {/* APPRISE MODE SELECTION */}
-
- {/* APPRISE TEST BUTTON */}
- {testIsLoading ? (
-
- ) : (
- onSendTestAppriseHandler()}
- >
- Send a test notification
-
- )}
- {info && (
-
- Notification successfully sent.
-
- )}
- {error && }
-
-
-
- >
- );
-}
diff --git a/Containers/UserSettings/AppriseAlertSettings/AppriseAlertSettings.tsx b/Containers/UserSettings/AppriseAlertSettings/AppriseAlertSettings.tsx
new file mode 100644
index 0000000..6b77206
--- /dev/null
+++ b/Containers/UserSettings/AppriseAlertSettings/AppriseAlertSettings.tsx
@@ -0,0 +1,171 @@
+import { IconExternalLink } from '@tabler/icons-react';
+import Link from 'next/link';
+import { useEffect, useState } from 'react';
+import { toast, ToastOptions } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import classes from '../UserSettings.module.css';
+
+//Components
+import Switch from '~/Components/UI/Switch/Switch';
+import { useLoader } from '~/contexts/LoaderContext';
+import { Optional } from '~/types';
+import AppriseMode from './AppriseMode/AppriseMode';
+import AppriseURLs from './AppriseURLs/AppriseURLs';
+
+type AppriseAlertDataForm = {
+ appriseAlert: boolean;
+};
+
+export default function AppriseAlertSettings() {
+ const toastOptions: ToastOptions = {
+ position: 'top-right',
+ autoClose: 5000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ };
+
+ const { start, stop } = useLoader();
+
+ ////State
+ const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
+ const [isSwitchDisabled, setIsSwitchDisabled] = useState(true);
+ const [isAlertEnabled, setIsAlertEnabled] = useState>(undefined);
+ const [info, setInfo] = useState(false);
+
+ ////LifeCycle
+ //Component did mount
+ useEffect(() => {
+ //Initial fetch to get the status of Apprise Alert
+ const getAppriseAlert = async () => {
+ try {
+ const response = await fetch('/api/v1/notif/apprise/alert', {
+ method: 'GET',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ });
+
+ const data: Optional = await response.json();
+ setIsAlertEnabled(data?.appriseAlert ?? false);
+ setIsSwitchDisabled(false);
+ } catch (error) {
+ setIsSwitchDisabled(true);
+ setIsAlertEnabled(false);
+ toast.error('Fetching Apprise alert setting failed', toastOptions);
+ }
+ };
+ getAppriseAlert();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ ////Functions
+ //Switch to enable/disable Apprise notifications
+ const onChangeSwitchHandler = async (data: AppriseAlertDataForm) => {
+ start();
+ setIsSwitchDisabled(true);
+ await fetch('/api/v1/notif/apprise/alert', {
+ method: 'PUT',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ })
+ .then((response) => {
+ if (response.ok && typeof data.appriseAlert === 'boolean') {
+ setIsAlertEnabled(data.appriseAlert);
+ toast.success(
+ data.appriseAlert ? 'Apprise notifications enabled' : 'Apprise notifications disabled',
+ toastOptions
+ );
+ } else {
+ toast.error('Update Apprise failed', toastOptions);
+ }
+ })
+ .catch(() => {
+ toast.error('Update Apprise failed', toastOptions);
+ })
+ .finally(() => {
+ stop();
+ setIsSwitchDisabled(false);
+ });
+ };
+
+ //Send Apprise test notification to services
+ const onSendTestAppriseHandler = async () => {
+ start();
+ setIsSendingTestNotification(true);
+ try {
+ const response = await fetch('/api/v1/notif/apprise/test', {
+ method: 'POST',
+ });
+ const result = await response.json();
+
+ if (!response.ok) {
+ toast.error(result.message, toastOptions);
+ } else {
+ setInfo(true);
+ setTimeout(() => {
+ setInfo(false);
+ }, 4000);
+ }
+ } catch (error) {
+ toast.error('Sending test notification failed', toastOptions);
+ } finally {
+ stop();
+ setIsSendingTestNotification(false);
+ }
+ };
+
+ return (
+ <>
+ {/* APPRISE ALERT */}
+
+
+
Apprise alert
+
+
+
+
+
+
+
onChangeSwitchHandler({ appriseAlert: e })}
+ />
+ {isAlertEnabled && (
+ <>
+
+
+ onSendTestAppriseHandler()}
+ >
+ Send a test notification
+
+ {info && (
+
+ Notification successfully sent.
+
+ )}
+ >
+ )}
+
+
+
+ >
+ );
+}
diff --git a/Containers/UserSettings/AppriseAlertSettings/AppriseMode/AppriseMode.js b/Containers/UserSettings/AppriseAlertSettings/AppriseMode/AppriseMode.js
deleted file mode 100644
index 66986ad..0000000
--- a/Containers/UserSettings/AppriseAlertSettings/AppriseMode/AppriseMode.js
+++ /dev/null
@@ -1,173 +0,0 @@
-//Lib
-import { useEffect } from 'react';
-import classes from '../../UserSettings.module.css';
-import { useState } from 'react';
-import { SpinnerCircularFixed } from 'spinners-react';
-import { useForm } from 'react-hook-form';
-
-//Components
-import Error from '../../../../Components/UI/Error/Error';
-
-export default function AppriseMode() {
- //Var
- const {
- register,
- handleSubmit,
- formState: { errors },
- } = useForm({ mode: 'onBlur' });
-
- ////State
- const [formIsLoading, setFormIsLoading] = useState(false);
- const [modeFormIsSaved, setModeFormIsSaved] = useState(false);
- const [error, setError] = useState(false);
- const [displayStatelessURL, setDisplayStatelessURL] = useState(false);
- const [appriseMode, setAppriseMode] = useState('stateless');
- const [appriseStatelessURL, setAppriseStatelessURL] = useState();
-
- ////LifeCycle
- //Component did mount
- useEffect(() => {
- //Initial fetch to get Apprise Mode enabled
- const getAppriseMode = async () => {
- try {
- const response = await fetch('/api/account/getAppriseMode', {
- method: 'GET',
- headers: {
- 'Content-type': 'application/json',
- },
- });
- const { appriseStatelessURL, appriseMode } =
- await response.json();
- setAppriseMode(appriseMode);
- if (appriseMode == 'stateless') {
- setAppriseStatelessURL(appriseStatelessURL);
- setDisplayStatelessURL(true);
- }
- } catch (error) {
- console.log('Fetching Apprise Mode failed.');
- }
- };
- getAppriseMode();
- }, []);
-
- ////Functions
- //Form submit handler to modify Apprise Mode
- const modeFormSubmitHandler = async (data) => {
- //Remove old error
- setError();
- //Loading button on submit to avoid multiple send.
- setFormIsLoading(true);
- //POST API to update Apprise Mode
- try {
- const response = await fetch('/api/account/updateAppriseMode', {
- method: 'PUT',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify(data),
- });
- const result = await response.json();
-
- if (!response.ok) {
- setFormIsLoading(false);
- setError(result.message);
- setTimeout(() => setError(), 4000);
- } else {
- setFormIsLoading(false);
- setModeFormIsSaved(true);
- setTimeout(() => setModeFormIsSaved(false), 3000);
- }
- } catch (error) {
- setFormIsLoading(false);
- setError('Change mode failed. Contact your administrator.');
- setTimeout(() => {
- setError();
- }, 4000);
- }
- };
-
- return (
- <>
- {/* APPRISE MODE SELECTION */}
-
-
Apprise mode
-
- {formIsLoading && (
-
- )}
- {modeFormIsSaved && (
-
- โ
Apprise mode has been saved.
-
- )}
-
-
- {error && }
-
-
- {displayStatelessURL && (
-
- )}
- {errors.appriseStatelessURL && (
-
- {errors.appriseStatelessURL.message}
-
- )}
-
- >
- );
-}
diff --git a/Containers/UserSettings/AppriseAlertSettings/AppriseMode/AppriseMode.tsx b/Containers/UserSettings/AppriseAlertSettings/AppriseMode/AppriseMode.tsx
new file mode 100644
index 0000000..2da7af1
--- /dev/null
+++ b/Containers/UserSettings/AppriseAlertSettings/AppriseMode/AppriseMode.tsx
@@ -0,0 +1,147 @@
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { AppriseModeDTO, AppriseModeEnum, Optional } from '~/types';
+import classes from '../../UserSettings.module.css';
+
+//Components
+import Error from '~/Components/UI/Error/Error';
+import { useLoader } from '~/contexts/LoaderContext';
+import { useFormStatus } from '~/hooks';
+
+type AppriseModeDataForm = {
+ appriseMode: string;
+ appriseStatelessURL: string;
+};
+
+export default function AppriseMode() {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({ mode: 'onChange' });
+
+ const { error, setIsLoading, handleSuccess, handleError, clearError } = useFormStatus();
+ const { start, stop } = useLoader();
+
+ const [displayStatelessURL, setDisplayStatelessURL] = useState(false);
+ const [appriseMode, setAppriseMode] = useState>(
+ AppriseModeEnum.STATELESS
+ );
+ const [appriseStatelessURL, setAppriseStatelessURL] = useState>();
+
+ //Component did mount
+ useEffect(() => {
+ //Initial fetch to get Apprise Mode enabled
+ const getAppriseMode = async () => {
+ try {
+ const response = await fetch('/api/v1/notif/apprise/mode', {
+ method: 'GET',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ });
+
+ const data: AppriseModeDTO = await response.json();
+ const { appriseStatelessURL, appriseMode } = data;
+ setAppriseMode(appriseMode);
+
+ if (appriseMode == AppriseModeEnum.STATELESS) {
+ setAppriseStatelessURL(appriseStatelessURL);
+ setDisplayStatelessURL(true);
+ }
+ } catch (error) {
+ console.log('Fetching Apprise Mode failed.');
+ }
+ };
+ getAppriseMode();
+ }, []);
+
+ ////Functions
+ const modeFormSubmitHandler = async (data: AppriseModeDataForm) => {
+ clearError();
+ setIsLoading(true);
+ start();
+
+ try {
+ const response = await fetch('/api/v1/notif/apprise/mode', {
+ method: 'PUT',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+ const result = await response.json();
+
+ if (!response.ok) {
+ handleError(result.message);
+ } else {
+ handleSuccess();
+ }
+ } catch (error) {
+ handleError('The Apprise mode change has failed');
+ } finally {
+ stop();
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <>
+ {/* APPRISE MODE SELECTION */}
+
+ {error && }
+
+
+ {displayStatelessURL && (
+
+ )}
+ {errors.appriseStatelessURL && (
+ {errors.appriseStatelessURL.message}
+ )}
+
+ >
+ );
+}
diff --git a/Containers/UserSettings/AppriseAlertSettings/AppriseURLs/AppriseURLs.js b/Containers/UserSettings/AppriseAlertSettings/AppriseURLs/AppriseURLs.js
deleted file mode 100644
index 44716ad..0000000
--- a/Containers/UserSettings/AppriseAlertSettings/AppriseURLs/AppriseURLs.js
+++ /dev/null
@@ -1,163 +0,0 @@
-//Lib
-import { useEffect } from 'react';
-import classes from '../../UserSettings.module.css';
-import { useState } from 'react';
-import { SpinnerCircularFixed } from 'spinners-react';
-import { useForm } from 'react-hook-form';
-
-//Components
-import Error from '../../../../Components/UI/Error/Error';
-
-export default function AppriseURLs() {
- //Var
- const {
- register,
- handleSubmit,
- formState: { errors },
- } = useForm({ mode: 'onBlur' });
-
- ////State
- const [formIsLoading, setFormIsLoading] = useState(false);
- const [urlsFormIsSaved, setUrlsFormIsSaved] = useState(false);
- const [appriseServicesList, setAppriseServicesList] = useState();
- const [error, setError] = useState();
-
- ////LifeCycle
- //Component did mount
- useEffect(() => {
- //Initial fetch to build the list of Apprise Services enabled
- const getAppriseServices = async () => {
- try {
- const response = await fetch(
- '/api/account/getAppriseServices',
- {
- method: 'GET',
- headers: {
- 'Content-type': 'application/json',
- },
- }
- );
- let servicesArray = (await response.json()).appriseServices;
- const AppriseServicesListToText = () => {
- let list = '';
- for (let service of servicesArray) {
- list += service + '\n';
- }
- return list;
- };
- setAppriseServicesList(AppriseServicesListToText());
- } catch (error) {
- console.log('Fetching Apprise services list failed.');
- }
- };
- getAppriseServices();
- }, []);
-
- ////Functions
- //Form submit handler to modify Apprise services
- const urlsFormSubmitHandler = async (data) => {
- //Remove old error
- setError();
- //Loading button on submit to avoid multiple send.
- setFormIsLoading(true);
- //POST API to update Apprise Services
- try {
- const response = await fetch('/api/account/updateAppriseServices', {
- method: 'PUT',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify(data),
- });
- const result = await response.json();
-
- if (!response.ok) {
- setFormIsLoading(false);
- setError(result.message);
- setTimeout(() => setError(), 4000);
- } else {
- setFormIsLoading(false);
- setUrlsFormIsSaved(true);
- setTimeout(() => setUrlsFormIsSaved(false), 3000);
- }
- } catch (error) {
- setFormIsLoading(false);
- setError(
- 'Failed to update your services. Contact your administrator.'
- );
- setTimeout(() => {
- setError();
- }, 4000);
- }
- };
-
- return (
- <>
- {/* APPRISE SERVICES URLS */}
-
-
Apprise URLs
- {error &&
}
-
- {formIsLoading && (
-
- )}
- {urlsFormIsSaved && (
-
- โ
Apprise configuration has been saved.
-
- )}
-
-
-
-
- {errors.appriseURLs && (
-
- {errors.appriseURLs.message}
-
- )}
-
-
- Use{' '}
-
- Apprise URLs
- {' '}
- to send a notification to any service. Only one URL per line.
-
- >
- );
-}
diff --git a/Containers/UserSettings/AppriseAlertSettings/AppriseURLs/AppriseURLs.tsx b/Containers/UserSettings/AppriseAlertSettings/AppriseURLs/AppriseURLs.tsx
new file mode 100644
index 0000000..e5b87f7
--- /dev/null
+++ b/Containers/UserSettings/AppriseAlertSettings/AppriseURLs/AppriseURLs.tsx
@@ -0,0 +1,142 @@
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { AppriseServicesDTO, Optional } from '~/types';
+import classes from '../../UserSettings.module.css';
+
+//Components
+import Error from '~/Components/UI/Error/Error';
+import { useLoader } from '~/contexts/LoaderContext';
+import { useFormStatus } from '~/hooks';
+
+type AppriseURLsDataForm = {
+ appriseURLs: string;
+};
+
+export default function AppriseURLs() {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({ mode: 'onBlur' });
+
+ const { isSaved, error, handleSuccess, handleError, clearError } = useFormStatus();
+ const { start, stop } = useLoader();
+
+ const [appriseServicesList, setAppriseServicesList] = useState>();
+ const [fetchError, setFetchError] = useState>();
+
+ //Component did mount
+ useEffect(() => {
+ //Initial fetch to build the list of Apprise Services enabled
+ const getAppriseServices = async () => {
+ try {
+ const response = await fetch('/api/v1/notif/apprise/services', {
+ method: 'GET',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ });
+
+ const data: AppriseServicesDTO = await response.json();
+ const servicesText = data.appriseServices?.join('\n');
+ setAppriseServicesList(servicesText);
+ setFetchError(false);
+ } catch (error) {
+ setFetchError(true);
+ handleError('Fetching Apprise services list failed.');
+ }
+ };
+ getAppriseServices();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ //Form submit handler to modify Apprise services
+ const urlsFormSubmitHandler = async (data: AppriseURLsDataForm) => {
+ clearError();
+ start();
+ if (fetchError) {
+ handleError('Cannot update Apprise services. Failed to fetch the initial list.');
+ stop();
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/v1/notif/apprise/services', {
+ method: 'PUT',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+ const result = await response.json();
+
+ if (!response.ok) {
+ handleError(result.message);
+ } else {
+ handleSuccess();
+ }
+ } catch (error) {
+ handleError('Failed to update your Apprise services.');
+ } finally {
+ stop();
+ }
+ };
+
+ return (
+ <>
+ {/* APPRISE SERVICES URLS */}
+
+
Apprise URLs
+
+ {isSaved && (
+
+ โ
Apprise configuration has been saved.
+
+ )}
+
+
+
+
+ {errors.appriseURLs && (
+ {errors.appriseURLs.message}
+ )}
+
+
+ Use{' '}
+
+ Apprise URLs
+ {' '}
+ to send a notification to any service. Only one URL per line.
+
+ {error && }
+ >
+ );
+}
diff --git a/Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.js b/Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.js
deleted file mode 100644
index ff2a717..0000000
--- a/Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.js
+++ /dev/null
@@ -1,208 +0,0 @@
-//Lib
-import { useEffect } from 'react';
-import { toast } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-import classes from '../UserSettings.module.css';
-import { useState } from 'react';
-import { SpinnerCircularFixed } from 'spinners-react';
-import { IconExternalLink } from '@tabler/icons-react';
-import Link from 'next/link';
-
-//Components
-import Error from '../../../Components/UI/Error/Error';
-import Switch from '../../../Components/UI/Switch/Switch';
-
-export default function EmailAlertSettings() {
- //Var
- const toastOptions = {
- position: 'top-right',
- autoClose: 5000,
- hideProgressBar: false,
- closeOnClick: true,
- pauseOnHover: true,
- draggable: true,
- progress: undefined,
- //Callback > re-enabled button after notification.
- onClose: () => setDisabled(false),
- };
-
- ////State
- const [isLoading, setIsLoading] = useState(true);
- const [testIsLoading, setTestIsLoading] = useState(false);
- const [error, setError] = useState();
- const [disabled, setDisabled] = useState(false);
- const [checked, setChecked] = useState();
- const [info, setInfo] = useState(false);
-
- ////LifeCycle
- //Component did mount
- useEffect(() => {
- const dataFetch = async () => {
- try {
- const response = await fetch('/api/account/getEmailAlert', {
- method: 'GET',
- headers: {
- 'Content-type': 'application/json',
- },
- });
- setChecked((await response.json()).emailAlert);
- setIsLoading(false);
- } catch (error) {
- setError(
- 'Fetching email alert setting failed. Contact your administrator.'
- );
- console.log('Fetching email alert setting failed.');
- setIsLoading(false);
- }
- };
- dataFetch();
- }, []);
-
- ////Functions
- //Switch to enable/disable Email notifications
- const onChangeSwitchHandler = async (data) => {
- //Remove old error
- setError();
- //Disabled button
- setDisabled(true);
- await fetch('/api/account/updateEmailAlert', {
- method: 'PUT',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify(data),
- })
- .then((response) => {
- console.log(response);
- if (response.ok) {
- if (data.emailAlert) {
- setChecked(!checked);
- toast.success(
- 'Email notification enabled !',
- toastOptions
- );
- } else {
- setChecked(!checked);
- toast.success(
- 'Email notification disabled !',
- toastOptions
- );
- }
- } else {
- setError('Update email alert setting failed.');
- setTimeout(() => {
- setError();
- setDisabled(false);
- }, 4000);
- }
- })
- .catch((error) => {
- console.log(error);
- setError('Update failed. Contact your administrator.');
- setTimeout(() => {
- setError();
- setDisabled(false);
- }, 4000);
- });
- };
-
- //Send a test notification by email
- const onSendTestMailHandler = async () => {
- //Loading
- setTestIsLoading(true);
- //Remove old error
- setError();
- await fetch('/api/account/sendTestEmail', {
- method: 'POST',
- })
- .then((response) => {
- if (!response.ok) {
- setTestIsLoading(false);
- setError('Failed to send the notification.');
- setTimeout(() => {
- setError();
- }, 4000);
- } else {
- setTestIsLoading(false);
- setInfo(true);
- setTimeout(() => {
- setInfo(false);
- }, 4000);
- }
- })
- .catch((error) => {
- setTestIsLoading(false);
- console.log(error);
- setError('Send email failed. Contact your administrator.');
- setTimeout(() => {
- setError();
- }, 4000);
- });
- };
-
- return (
- <>
- {/* EMAIL ALERT */}
-
-
-
Email alert
-
-
-
-
-
-
- {isLoading ? (
-
- ) : (
-
- onChangeSwitchHandler({ emailAlert: e })
- }
- />
- )}
- {testIsLoading ? (
-
- ) : (
-
- Send a test mail
-
- )}
- {info && (
-
- Mail successfully sent.
-
- )}
- {error && }
-
-
-
- >
- );
-}
diff --git a/Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.tsx b/Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.tsx
new file mode 100644
index 0000000..ff67786
--- /dev/null
+++ b/Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.tsx
@@ -0,0 +1,169 @@
+import { IconExternalLink } from '@tabler/icons-react';
+import Link from 'next/link';
+import { useEffect, useState } from 'react';
+import { toast, ToastOptions } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { useLoader } from '~/contexts/LoaderContext';
+import { EmailAlertDTO, Optional } from '~/types';
+import classes from '../UserSettings.module.css';
+
+//Components
+import Error from '~/Components/UI/Error/Error';
+import Switch from '~/Components/UI/Switch/Switch';
+import { useFormStatus } from '~/hooks';
+
+export default function EmailAlertSettings() {
+ const toastOptions: ToastOptions = {
+ position: 'top-right',
+ autoClose: 5000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ //Callback > re-enabled button after notification.
+ onClose: () => setIsSwitchDisabled(false),
+ };
+
+ const { error, handleError, clearError } = useFormStatus();
+ const { start, stop } = useLoader();
+
+ ////State
+ const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
+ const [isSwitchDisabled, setIsSwitchDisabled] = useState(true);
+ const [isAlertEnabled, setIsAlertEnabled] = useState>(undefined);
+ const [info, setInfo] = useState(false);
+
+ ////LifeCycle
+ //Component did mount
+ useEffect(() => {
+ const dataFetch = async () => {
+ try {
+ const response = await fetch('/api/v1/notif/email/alert', {
+ method: 'GET',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ });
+
+ const data: Optional = await response.json();
+ setIsAlertEnabled(data?.emailAlert ?? false);
+ setIsSwitchDisabled(false);
+ } catch (error) {
+ setIsSwitchDisabled(true);
+ setIsAlertEnabled(false);
+ handleError('Fetching email alert setting failed');
+ }
+ };
+ dataFetch();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ ////Functions
+ //Switch to enable/disable Email notifications
+ const onChangeSwitchHandler = async (data: EmailAlertDTO) => {
+ clearError();
+ start();
+ setIsSwitchDisabled(true);
+ await fetch('/api/v1/notif/email/alert', {
+ method: 'PUT',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ })
+ .then((response) => {
+ if (response.ok && typeof data.emailAlert === 'boolean') {
+ setIsAlertEnabled(data.emailAlert);
+ toast.success(
+ data.emailAlert ? 'Email notification enabled !' : 'Email notification disabled !',
+ toastOptions
+ );
+ } else {
+ handleError('Update email alert setting failed.');
+ }
+ })
+ .catch(() => {
+ handleError('Update email alert setting failed.');
+ })
+ .finally(() => {
+ stop();
+ setIsSwitchDisabled(false);
+ });
+ };
+
+ //Send a test notification by email
+ const onSendTestMailHandler = async () => {
+ clearError();
+ start();
+ setIsSendingTestNotification(true);
+ try {
+ const response = await fetch('/api/v1/notif/email/test', {
+ method: 'POST',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ });
+ const result = await response.json();
+
+ if (!response.ok) {
+ setIsSendingTestNotification(false);
+ handleError(result.message);
+ } else {
+ setIsSendingTestNotification(false);
+ setInfo(true);
+ setTimeout(() => {
+ setInfo(false);
+ }, 4000);
+ }
+ } catch (error) {
+ setIsSendingTestNotification(false);
+ handleError('Send notification failed');
+ } finally {
+ stop();
+ }
+ };
+
+ return (
+ <>
+ {/* EMAIL ALERT */}
+
+
+
Email alert
+
+
+
+
+
+
+ onChangeSwitchHandler({ emailAlert: e })}
+ />
+
+
+ Send a test mail
+
+ {info && (
+ Mail successfully sent.
+ )}
+ {error && }
+
+
+
+ >
+ );
+}
diff --git a/Containers/UserSettings/EmailSettings/EmailSettings.js b/Containers/UserSettings/EmailSettings/EmailSettings.js
deleted file mode 100644
index 71731f2..0000000
--- a/Containers/UserSettings/EmailSettings/EmailSettings.js
+++ /dev/null
@@ -1,138 +0,0 @@
-//Lib
-import { toast } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-import classes from '../UserSettings.module.css';
-import { useState } from 'react';
-import { useForm } from 'react-hook-form';
-import { SpinnerDotted } from 'spinners-react';
-
-//Components
-import Error from '../../../Components/UI/Error/Error';
-import Info from '../../../Components/UI/Info/Info';
-
-export default function EmailSettings(props) {
- //Var
- const toastOptions = {
- position: 'top-right',
- autoClose: 8000,
- hideProgressBar: false,
- closeOnClick: true,
- pauseOnHover: true,
- draggable: true,
- progress: undefined,
- };
-
- const {
- register,
- handleSubmit,
- reset,
- formState: { errors, isSubmitting, isValid },
- } = useForm({ mode: 'onChange' });
-
- ////State
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState();
- const [info, setInfo] = useState(false);
-
- ////Functions
- //Form submit Handler for ADD a repo
- const formSubmitHandler = async (data) => {
- //Remove old error
- setError();
- //Loading button on submit to avoid multiple send.
- setIsLoading(true);
- //POST API to send the new mail address
- try {
- const response = await fetch('/api/account/updateEmail', {
- method: 'PUT',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify(data),
- });
- const result = await response.json();
-
- if (!response.ok) {
- setIsLoading(false);
- reset();
- setError(result.message);
- setTimeout(() => setError(), 4000);
- } else {
- reset();
- setIsLoading(false);
- setInfo(true);
- toast.success('Email edited !', toastOptions);
- }
- } catch (error) {
- reset();
- setIsLoading(false);
- setError("Can't update your email. Contact your administrator.");
- setTimeout(() => setError(), 4000);
- }
- };
- return (
- <>
- {/* EMAIL */}
-
-
-
Email
-
-
-
- {info ? ( //For local JWTs (cookie) without an OAuth provider, Next-Auth does not allow
- //at the time this code is written to refresh client-side session information
- //without triggering a logout.
- //I chose to inform the user to reconnect rather than force logout.
-
- ) : (
-
-
- {error && }
- ()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
- message:
- 'Your email is not valid.',
- },
- })}
- />
- {errors.email && (
-
- {errors.email.message}
-
- )}
-
-
- {isLoading ? (
-
- ) : (
- 'Update your email'
- )}
-
-
- )}
-
-
-
- >
- );
-}
diff --git a/Containers/UserSettings/EmailSettings/EmailSettings.tsx b/Containers/UserSettings/EmailSettings/EmailSettings.tsx
new file mode 100644
index 0000000..57607dc
--- /dev/null
+++ b/Containers/UserSettings/EmailSettings/EmailSettings.tsx
@@ -0,0 +1,121 @@
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast, ToastOptions } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import classes from '../UserSettings.module.css';
+
+//Components
+import Error from '~/Components/UI/Error/Error';
+import Info from '~/Components/UI/Info/Info';
+import { useLoader } from '~/contexts/LoaderContext';
+import { useFormStatus } from '~/hooks';
+import { EmailSettingDTO } from '~/types/api/setting.types';
+
+export default function EmailSettings(props: EmailSettingDTO) {
+ const toastOptions: ToastOptions = {
+ position: 'top-right',
+ autoClose: 8000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ };
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, isSubmitting },
+ } = useForm({ mode: 'onChange' });
+
+ const { isLoading, error, setIsLoading, handleError, clearError } = useFormStatus();
+ const { start, stop } = useLoader();
+
+ ////State
+ const [info, setInfo] = useState(false);
+
+ ////Functions
+ const formSubmitHandler = async (data: EmailSettingDTO) => {
+ start();
+ clearError();
+ setIsLoading(true);
+
+ try {
+ const response = await fetch('/api/v1/account/email', {
+ method: 'PUT',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+ const result = await response.json();
+
+ if (!response.ok) {
+ reset();
+ handleError(result.message);
+ } else {
+ reset();
+ setIsLoading(false);
+ setInfo(true);
+ toast.success('Email edited !', toastOptions);
+ }
+ } catch (error) {
+ reset();
+ handleError('Updating your email failed.');
+ } finally {
+ stop();
+ setIsLoading(false);
+ }
+ };
+ return (
+ <>
+ {/* EMAIL */}
+
+
+
Email
+
+
+
+ {info ? ( //For local JWTs (cookie) without an OAuth provider, Next-Auth does not allow
+ //at the time this code is written to refresh client-side session information
+ //without triggering a logout.
+ //I chose to inform the user to reconnect rather than force logout.
+
+ ) : (
+
+
+ {error && }
+ ()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
+ message: 'Your email is not valid.',
+ },
+ })}
+ />
+ {errors.email && (
+ {errors.email.message}
+ )}
+
+
+ Update your email
+
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/Containers/UserSettings/Integrations/Integrations.tsx b/Containers/UserSettings/Integrations/Integrations.tsx
new file mode 100644
index 0000000..90c5748
--- /dev/null
+++ b/Containers/UserSettings/Integrations/Integrations.tsx
@@ -0,0 +1,340 @@
+import { IconExternalLink, IconTrash } from '@tabler/icons-react';
+import { fromUnixTime } from 'date-fns';
+import Link from 'next/link';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast, ToastOptions } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { useFormStatus } from '~/hooks';
+import { IntegrationTokenType, Optional, TokenPermissionEnum, TokenPermissionsType } from '~/types';
+import classes from '../UserSettings.module.css';
+
+//Components
+import CopyButton from '~/Components/UI/CopyButton/CopyButton';
+import Error from '~/Components/UI/Error/Error';
+import Info from '~/Components/UI/Info/Info';
+import { useLoader } from '~/contexts/LoaderContext';
+
+type IntegrationsDataForm = {
+ tokenName: string;
+};
+
+export default function Integrations() {
+ const toastOptions: ToastOptions = {
+ position: 'top-right',
+ autoClose: 5000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ };
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, isSubmitting, isValid },
+ } = useForm({ mode: 'onChange' });
+ const { start, stop } = useLoader();
+
+ const { error, handleError, clearError, setIsLoading, isLoading } = useFormStatus();
+
+ const renderPermissionBadges = (permissions: TokenPermissionsType) => {
+ return Object.entries(permissions)
+ .filter(([, hasPermission]) => hasPermission)
+ .map(([key]) => (
+
+ {key.charAt(0).toUpperCase() + key.slice(1)}
+
+ ));
+ };
+
+ ////State
+ const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+ const [tokenList, setTokenList] = useState>();
+ const [lastGeneratedToken, setLastGeneratedToken] =
+ useState>();
+ const [deletingToken, setDeletingToken] = useState>(undefined);
+ const [permissions, setPermissions] = useState({
+ create: false,
+ read: false,
+ update: false,
+ delete: false,
+ });
+
+ const fetchTokenList = async () => {
+ start();
+ try {
+ const response = await fetch('/api/v1/integration/token-manager', {
+ method: 'GET',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ });
+ const data: Array = await response.json();
+ setTokenList(data);
+ } catch (error) {
+ handleError('Fetching token list failed.');
+ } finally {
+ stop();
+ }
+ };
+
+ ////LifeCycle
+ useEffect(() => {
+ fetchTokenList();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // Permissions handler
+ const hasNoPermissionSelected = () => {
+ return !Object.values(permissions).some((value) => value);
+ };
+ const togglePermission = (permissionType: TokenPermissionEnum) => {
+ const updatedPermissions = {
+ ...permissions,
+ [permissionType]: !permissions[permissionType],
+ };
+ setPermissions(updatedPermissions);
+ };
+ const resetPermissions = () => {
+ setPermissions({
+ create: false,
+ read: false,
+ update: false,
+ delete: false,
+ });
+ };
+
+ //Form submit handler to ADD a new token
+ const formSubmitHandler = async (data: IntegrationsDataForm) => {
+ start();
+ clearError();
+ setIsLoading(true);
+
+ // Post API to send the new token integration
+ try {
+ const response = await fetch('/api/v1/integration/token-manager', {
+ method: 'POST',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: data.tokenName,
+ permissions: permissions,
+ }),
+ });
+ const result = await response.json();
+ setLastGeneratedToken({ name: data.tokenName, value: result.token });
+
+ if (!response.ok) {
+ toast.error(result.message, toastOptions);
+ } else {
+ fetchTokenList();
+ toast.success('๐ Token generated !', toastOptions);
+ }
+ } catch (error) {
+ toast.error('Failed to generate a new token', toastOptions);
+ } finally {
+ setIsLoading(false);
+ resetPermissions();
+ reset();
+ stop();
+ }
+ };
+
+ //Delete token
+ const deleteTokenHandler = async (tokenName: string) => {
+ setIsDeleteLoading(true);
+ try {
+ const response = await fetch('/api/v1/integration/token-manager', {
+ method: 'DELETE',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: tokenName,
+ }),
+ });
+ const result = await response.json();
+
+ if (!response.ok) {
+ toast.error(result.message, toastOptions);
+ setIsDeleteLoading(false);
+ } else {
+ fetchTokenList();
+ setIsDeleteLoading(false);
+ toast.success('๐๏ธ Token deleted !', toastOptions);
+ }
+ } catch (error) {
+ setIsDeleteLoading(false);
+ toast.error('Failed to delete the token', toastOptions);
+ } finally {
+ setIsDeleteLoading(false);
+ setDeletingToken(undefined);
+ }
+ };
+
+ return (
+ <>
+
+
+
Generate token
+
+
+
+
+
+
+
+
+
+
+
togglePermission(TokenPermissionEnum.CREATE)}
+ >
+ Create
+
+
togglePermission(TokenPermissionEnum.READ)}
+ >
+ Read
+
+
togglePermission(TokenPermissionEnum.UPDATE)}
+ >
+ Update
+
+
togglePermission(TokenPermissionEnum.DELETE)}
+ >
+ Delete
+
+
+
+
+
+ Generate
+
+
+ {errors.tokenName && errors.tokenName.type === 'maxLength' && (
+
25 characters max.
+ )}
+ {errors.tokenName && errors.tokenName.type === 'pattern' && (
+
+ Only alphanumeric characters, dashes, and underscores are allowed (no spaces).
+
+ )}
+ {error &&
}
+
+
+ {tokenList && tokenList.length > 0 && (
+
+
+
API Tokens
+
+
+ {tokenList
+ .slice()
+ .sort((a, b) => b.creation - a.creation)
+ .map((token, index) => (
+
+
+
{token.name}
+
+
+ Created at:
+ {fromUnixTime(token.creation).toLocaleString()}
+
+
+
Permission:
+
+ {renderPermissionBadges(token.permissions)}
+
+
+ {lastGeneratedToken && lastGeneratedToken.name === token.name && (
+ <>
+
+ Token:
+
+ {lastGeneratedToken.value}
+
+
+
+ >
+ )}
+ {deletingToken && deletingToken.name === token.name && (
+
+ deleteTokenHandler(token.name)}
+ disabled={isDeleteLoading}
+ >
+ Confirm
+
+ {!isDeleteLoading && (
+ setDeletingToken(undefined)}
+ >
+ Cancel
+
+ )}
+
+ )}
+
+
+
+ setDeletingToken(token)}
+ />
+
+
+ ))}
+
+
+ )}
+ >
+ );
+}
diff --git a/Containers/UserSettings/PasswordSettings/PasswordSettings.js b/Containers/UserSettings/PasswordSettings/PasswordSettings.js
deleted file mode 100644
index 0e684ea..0000000
--- a/Containers/UserSettings/PasswordSettings/PasswordSettings.js
+++ /dev/null
@@ -1,136 +0,0 @@
-//Lib
-import { toast } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-import classes from '../UserSettings.module.css';
-import { useState } from 'react';
-import { useForm } from 'react-hook-form';
-import { SpinnerDotted } from 'spinners-react';
-
-//Components
-import Error from '../../../Components/UI/Error/Error';
-
-export default function PasswordSettings(props) {
- //Var
- const toastOptions = {
- position: 'top-right',
- autoClose: 5000,
- hideProgressBar: false,
- closeOnClick: true,
- pauseOnHover: true,
- draggable: true,
- progress: undefined,
- };
-
- const {
- register,
- handleSubmit,
- reset,
- formState: { errors, isSubmitting, isValid },
- } = useForm({ mode: 'onChange' });
-
- ////State
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState();
-
- ////Functions
- //Form submit Handler for ADD a repo
- const formSubmitHandler = async (data) => {
- console.log(data);
-
- //Remove old error
- setError();
- //Loading button on submit to avoid multiple send.
- setIsLoading(true);
- //POST API to send the new and old password
- try {
- const response = await fetch('/api/account/updatePassword', {
- method: 'PUT',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify(data),
- });
- const result = await response.json();
-
- if (!response.ok) {
- setIsLoading(false);
- reset();
- setError(result.message);
- setTimeout(() => setError(), 4000);
- } else {
- reset();
- setIsLoading(false);
- toast.success('๐ Password edited !', toastOptions);
- }
- } catch (error) {
- reset();
- setIsLoading(false);
- setError("Can't update your password. Contact your administrator.");
- setTimeout(() => setError(), 4000);
- }
- };
- return (
- <>
- {/* PASSWORD */}
-
- >
- );
-}
diff --git a/Containers/UserSettings/PasswordSettings/PasswordSettings.tsx b/Containers/UserSettings/PasswordSettings/PasswordSettings.tsx
new file mode 100644
index 0000000..3af15fa
--- /dev/null
+++ b/Containers/UserSettings/PasswordSettings/PasswordSettings.tsx
@@ -0,0 +1,100 @@
+import { useForm } from 'react-hook-form';
+import { toast, ToastOptions } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { useFormStatus } from '~/hooks';
+import { PasswordSettingDTO } from '~/types';
+import classes from '../UserSettings.module.css';
+
+//Components
+import { useLoader } from '~/contexts/LoaderContext';
+
+export default function PasswordSettings() {
+ const toastOptions: ToastOptions = {
+ position: 'top-right',
+ autoClose: 5000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ };
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting },
+ } = useForm({ mode: 'onChange' });
+ const { start, stop } = useLoader();
+
+ const { isLoading, setIsLoading } = useFormStatus();
+
+ ////Functions
+ const formSubmitHandler = async (data: PasswordSettingDTO) => {
+ start();
+ setIsLoading(true);
+
+ try {
+ const response = await fetch('/api/v1/account/password', {
+ method: 'PUT',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+ const result = await response.json();
+
+ if (!response.ok) {
+ toast.error(result.message, toastOptions);
+ } else {
+ toast.success('๐ Password edited !', toastOptions);
+ }
+ } catch (error) {
+ toast.error('Failed to update password. Please try again.', toastOptions);
+ } finally {
+ stop();
+ reset();
+ setIsLoading(false);
+ }
+ };
+ return (
+ <>
+ {/* PASSWORD */}
+
+ >
+ );
+}
diff --git a/Containers/UserSettings/UserSettings.js b/Containers/UserSettings/UserSettings.js
deleted file mode 100644
index d83d761..0000000
--- a/Containers/UserSettings/UserSettings.js
+++ /dev/null
@@ -1,67 +0,0 @@
-//Lib
-import 'react-toastify/dist/ReactToastify.css';
-import classes from './UserSettings.module.css';
-import { useState } from 'react';
-
-//Components
-import EmailSettings from './EmailSettings/EmailSettings';
-import PasswordSettings from './PasswordSettings/PasswordSettings';
-import UsernameSettings from './UsernameSettings/UsernameSettings';
-import EmailAlertSettings from './EmailAlertSettings/EmailAlertSettings';
-import AppriseAlertSettings from './AppriseAlertSettings/AppriseAlertSettings';
-
-export default function UserSettings(props) {
- //States
- const [tab, setTab] = useState('General');
-
- return (
-
-
-
- Account{' '}
-
-
-
- setTab('General')}
- >
- General
-
- setTab('Notifications')}
- >
- Notifications
-
-
- {tab == 'General' && (
- <>
-
-
-
{' '}
- >
- )}
- {tab == 'Notifications' && (
- <>
-
-
- >
- )}
-
- );
-}
diff --git a/Containers/UserSettings/UserSettings.module.css b/Containers/UserSettings/UserSettings.module.css
index 6f9b393..5fd0010 100644
--- a/Containers/UserSettings/UserSettings.module.css
+++ b/Containers/UserSettings/UserSettings.module.css
@@ -1,251 +1,490 @@
.containerSettings {
- display: flex;
- flex-direction: column;
- width: 100%;
- max-width: 1000px;
- margin-top: 10px;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ max-width: 1000px;
+ margin-top: 10px;
}
.containerSetting {
- display: flex;
- flex-flow: row wrap;
- width: 100%;
- margin: 40px 20px 0px 5px;
- text-align: left;
- padding: 28px 24px;
- animation: entrance ease-in 0.3s 1 normal none;
- border-bottom: 1px solid #e5e7eb;
+ display: flex;
+ flex-flow: row wrap;
+ width: 100%;
+ margin: 40px 20px 0px 5px;
+ text-align: left;
+ padding: 28px 24px;
+ animation: entrance ease-in 0.3s 1 normal none;
+ border-bottom: 1px solid #e5e7eb;
}
@keyframes entrance {
- 0% {
- opacity: 0;
- }
+ 0% {
+ opacity: 0;
+ }
- 50% {
- opacity: 0.5;
- }
+ 50% {
+ opacity: 0.5;
+ }
- 100% {
- opacity: 1;
- }
+ 100% {
+ opacity: 1;
+ }
}
.settingCategory {
- max-width: 33.3333%;
- width: 100%;
- display: flex;
+ max-width: 33.3333%;
+ width: 100%;
+ display: flex;
}
.settingCategory h2 {
- color: #494b7a;
- margin: 0;
- font-size: 1.3em;
+ color: #494b7a;
+ margin: 0;
+ font-size: 1.3em;
}
.setting {
- max-width: 66.6666%;
- width: 100%;
+ max-width: 66.6666%;
+ width: 100%;
+}
+
+/* Tokens generation */
+
+.tokenGen {
+ display: flex;
+ align-items: baseline;
+ justify-content: flex-start;
+ gap: 10px;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.tokenGen input {
+ flex: 1;
+ margin-right: 10px;
+}
+
+.newTokenWrapper {
+ display: flex;
+ align-items: center;
+ background-color: #f5f5f5;
+ border-radius: 5px;
+ color: #494b7a;
+ outline: 1px solid #6d4aff;
+ box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
+ animation: entrance ease-in 0.3s 1 normal none;
+ padding: 10px;
+ font-family: (--pure-material-font, 'Roboto', 'Segoe UI', BlinkMacSystemFont, system-ui);
+}
+
+.tokenCardList {
+ min-width: 50%;
+}
+
+.tokenCardWrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 10px;
+ justify-content: space-between;
+ margin-bottom: 20px;
+}
+
+.tokenCard {
+ width: 100%;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ padding: 20px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+}
+
+.tokenCardHeader {
+ font-size: 1.2em;
+ margin-bottom: 10px;
+ border-bottom: 1px solid #eee;
+ padding-bottom: 5px;
+ color: #494b7a;
+}
+
+.tokenCardBody {
+ font-size: 0.9em;
+}
+
+.tokenCardBody .permissionBadges {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: baseline;
+ align-content: baseline;
+}
+
+.tokenInfo {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ margin: 10px 0;
+ color: #494b7a;
+}
+
+.tokenCardHighlight {
+ animation: highlightEffect 1s ease-out forwards;
+}
+
+@keyframes highlightEffect {
+ 0% {
+ outline: 1px solid #6d4aff;
+ box-shadow: 0 0 0 rgba(110, 74, 255, 0.5); /* Pas d'ombre au dรฉbut */
+ }
+ 50% {
+ outline: 1px solid #6d4aff;
+ box-shadow: 0 0 15px rgba(110, 74, 255, 0.6); /* Ombre qui s'agrandit */
+ }
+ 100% {
+ outline: 1px solid transparent; /* Bordure devient transparente */
+ box-shadow: 0;
+ }
+}
+
+.cancelButton {
+ border: 0;
+ padding: 10px 15px;
+ background-color: #c1c1c1;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
+}
+
+.cancelButton:hover {
+ border: 0;
+ padding: 10px 15px;
+ background-color: #9a9a9a;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
+}
+
+.cancelButton:active {
+ border: 0;
+ padding: 10px 15px;
+ background-color: #9a9a9a;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
+ transform: scale(0.9);
+}
+
+.deleteConfirmationButtons {
+ display: flex;
+ flex-direction: row;
+}
+
+.confirmButton {
+ border: 0;
+ padding: 10px 15px;
+ background-color: #ff0000;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.confirmButton:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+.confirmButton:hover {
+ border: 0;
+ padding: 10px 15px;
+ background-color: #ff4b4b;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
+}
+
+.confirmButton:active {
+ border: 0;
+ padding: 10px 15px;
+ background-color: #ff4b4b;
+ color: white;
+ margin: 5px;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 1em;
+ transform: scale(0.9);
+}
+
+.permissionBadge {
+ user-select: none;
+ border-radius: 5px;
+ border: 1px solid #6d4aff;
+ color: #6d4aff;
+ font-size: 0.9em;
+ padding: 2px 5px;
+ margin-right: 8px;
+}
+
+.tokenWrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 15px;
+}
+
+.permissionsWrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ width: 100%;
+ gap: 15px;
+}
+
+.permissionsWrapper .permissionBadge {
+ user-select: none;
+ border-radius: 5px;
+ border: 1px solid #9798b2;
+ color: #9798b2;
+ font-size: 0.9em;
+ margin: 0;
+ cursor: pointer;
+}
+
+.permissionsWrapper .permissionBadge.highlight {
+ border-radius: 5px;
+ border: 1px solid #6d4aff;
+ color: #6d4aff;
+ font-size: 0.9em;
+ margin: 0;
+ cursor: pointer;
}
/* Forms */
.bwForm {
- width: 80%;
- border-radius: 5px;
- text-align: left;
+ width: 80%;
+ border-radius: 5px;
+ text-align: left;
}
.bwFormWrapper {
- text-align: left;
- margin: auto;
- width: 100%;
- height: auto;
- color: #494b7a;
- font-family: var(
- --pure-material-font,
- 'Roboto',
- 'Segoe UI',
- BlinkMacSystemFont,
- system-ui,
- -apple-system
- );
+ text-align: left;
+ margin: auto;
+ width: 100%;
+ height: auto;
+ color: #494b7a;
+ font-family: var(
+ --pure-material-font,
+ 'Roboto',
+ 'Segoe UI',
+ BlinkMacSystemFont,
+ system-ui,
+ -apple-system
+ );
}
.bwFormWrapper p {
- margin-block-start: 0em;
+ margin-block-start: 0em;
}
.bwForm label {
- display: block;
- margin-bottom: 8px;
- text-align: center;
- /* margin-top: 20px; */
- color: #494b7a;
+ display: block;
+ margin-bottom: 8px;
+ text-align: center;
+ /* margin-top: 20px; */
+ color: #494b7a;
+}
+
+.bwForm.tokenGen label {
+ margin-bottom: 0px;
}
.bwForm input,
.bwForm textarea,
.bwForm select {
- border: 1px solid #6d4aff21;
- font-size: 16px;
- height: auto;
- margin: 0;
- margin-bottom: 0px;
- outline: 0;
- padding: 10px;
- width: 100%;
- background-color: #f5f5f5;
- border-radius: 5px;
- /* color: #1b1340; */
- color: #494b7a;
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
- font-family: (
- --pure-material-font,
- 'Roboto',
- 'Segoe UI',
- BlinkMacSystemFont,
- system-ui
- );
+ border: 1px solid #6d4aff21;
+ font-size: 16px;
+ height: auto;
+ margin: 0;
+ margin-bottom: 0px;
+ outline: 0;
+ padding: 10px;
+ width: 100%;
+ background-color: #f5f5f5;
+ border-radius: 5px;
+ /* color: #1b1340; */
+ color: #494b7a;
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
+ font-family: (--pure-material-font, 'Roboto', 'Segoe UI', BlinkMacSystemFont, system-ui);
}
.bwForm textarea {
- resize: vertical;
- overflow: auto;
- white-space: pre;
+ resize: vertical;
+ overflow: auto;
+ white-space: pre;
}
.bwForm textarea:focus,
.bwForm input:focus,
.bwForm select:focus {
- outline: 1px solid #6d4aff;
- box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
+ outline: 1px solid #6d4aff;
+ box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
}
.bwForm .invalid {
- background: #f3c7c7;
- border: 1px solid #e45454;
- outline: 1px solid #ff4a4a;
+ background: #f3c7c7;
+ border: 1px solid #e45454;
+ outline: 1px solid #ff4a4a;
}
.bwForm .invalid:focus {
- background: #f3c7c7;
- border: 1px solid #e45454;
- outline: 1px solid #ff4a4a;
- box-shadow: 0 0 10px 3px rgba(255, 74, 74, 0.605);
+ background: #f3c7c7;
+ border: 1px solid #e45454;
+ outline: 1px solid #ff4a4a;
+ box-shadow: 0 0 10px 3px rgba(255, 74, 74, 0.605);
}
.bwForm button {
- display: block;
+ display: block;
}
.bwForm button:hover {
- display: block;
+ display: block;
}
.errorMessage {
- color: red;
- display: block;
- margin-top: 3px;
+ color: red;
+ display: block;
+ margin-top: 3px;
}
.currentSetting input::placeholder {
- opacity: 1;
+ opacity: 1;
}
.headerFormAppriseUrls {
- font-weight: 500;
- color: #494b7a;
- margin: 40px 0px 10px 0px;
- display: flex;
- padding-right: 5px;
+ font-weight: 500;
+ color: #494b7a;
+ margin-bottom: 10px;
+ display: flex;
+ padding-right: 5px;
}
.formIsSavedMessage {
- color: rgb(0, 164, 0);
- animation: entrance 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
- font-weight: 300;
+ color: rgb(0, 164, 0);
+ animation: entrance 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
+ font-weight: 300;
}
.tabList {
- display: flex;
+ display: flex;
}
.tabListButton {
- color: #494b7a;
- padding: 12px 0px;
- min-height: 48px;
- overflow: hidden;
- text-align: center;
- flex-direction: column;
- font-size: 1em;
- font-weight: 500;
- line-height: 1.71;
- text-transform: none;
- align-items: center;
- cursor: pointer;
- vertical-align: middle;
- text-decoration: none;
- border: 0;
- background-color: transparent;
- margin-left: 30px;
- border-bottom: 2px solid transparent;
+ color: #494b7a;
+ padding: 12px 0px;
+ min-height: 48px;
+ overflow: hidden;
+ text-align: center;
+ flex-direction: column;
+ font-size: 1em;
+ font-weight: 500;
+ line-height: 1.71;
+ text-transform: none;
+ align-items: center;
+ cursor: pointer;
+ vertical-align: middle;
+ text-decoration: none;
+ border: 0;
+ background-color: transparent;
+ margin-left: 30px;
+ border-bottom: 2px solid transparent;
}
.tabListButton:hover {
- color: #6d4aff;
- border-bottom: 2px solid #6d4aff;
+ color: #6d4aff;
+ border-bottom: 2px solid #6d4aff;
}
.tabListButtonActive {
- color: #6d4aff;
- border: 0;
- border-bottom: 2px solid #6d4aff;
- padding: 12px 0px;
- min-height: 48px;
- overflow: hidden;
- text-align: center;
- flex-direction: column;
- font-size: 1em;
- font-weight: 500;
- line-height: 1.71;
- text-transform: none;
- align-items: center;
- cursor: pointer;
- vertical-align: middle;
- text-decoration: none;
- background-color: transparent;
- margin-left: 30px;
+ color: #6d4aff;
+ border: 0;
+ border-bottom: 2px solid #6d4aff;
+ padding: 12px 0px;
+ min-height: 48px;
+ overflow: hidden;
+ text-align: center;
+ flex-direction: column;
+ font-size: 1em;
+ font-weight: 500;
+ line-height: 1.71;
+ text-transform: none;
+ align-items: center;
+ cursor: pointer;
+ vertical-align: middle;
+ text-decoration: none;
+ background-color: transparent;
+ margin-left: 30px;
}
.AccountSettingsButton {
- border: 0;
- padding: 10px 15px;
- background-color: #6d4aff;
- color: white;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-size: 1em;
+ border: 0;
+ padding: 10px 15px;
+ background-color: #6d4aff;
+ color: white;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-size: 1em;
}
.AccountSettingsButton:hover {
- border: 0;
- padding: 10px 15px;
- background-color: #4f31ce;
- color: white;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-size: 1em;
+ border: 0;
+ padding: 10px 15px;
+ background-color: #4f31ce;
+ color: white;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-size: 1em;
}
.AccountSettingsButton:active {
- border: 0;
- padding: 10px 15px;
- background-color: #4f31ce;
- color: white;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-size: 1em;
- transform: scale(0.95);
+ border: 0;
+ padding: 10px 15px;
+ background-color: #4f31ce;
+ color: white;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-size: 1em;
+ transform: scale(0.95);
+}
+
+.AccountSettingsButton:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+ pointer-events: none;
}
diff --git a/Containers/UserSettings/UserSettings.tsx b/Containers/UserSettings/UserSettings.tsx
new file mode 100644
index 0000000..a561fb2
--- /dev/null
+++ b/Containers/UserSettings/UserSettings.tsx
@@ -0,0 +1,100 @@
+import 'react-toastify/dist/ReactToastify.css';
+import classes from './UserSettings.module.css';
+import { useState, useEffect } from 'react';
+import { Session } from 'next-auth';
+import { Optional, WizardEnvType, SessionStatus } from '~/types';
+
+// Components
+import EmailSettings from './EmailSettings/EmailSettings';
+import PasswordSettings from './PasswordSettings/PasswordSettings';
+import UsernameSettings from './UsernameSettings/UsernameSettings';
+import EmailAlertSettings from './EmailAlertSettings/EmailAlertSettings';
+import AppriseAlertSettings from './AppriseAlertSettings/AppriseAlertSettings';
+import Integrations from './Integrations/Integrations';
+
+type UserSettingsProps = {
+ status: SessionStatus;
+ data: Session;
+};
+
+export default function UserSettings({ data }: UserSettingsProps) {
+ const [tab, setTab] = useState<'General' | 'Notifications' | 'Integrations'>('General');
+ const [wizardEnv, setWizardEnv] = useState>(undefined);
+
+ // Fetch wizard environment on mount
+ useEffect(() => {
+ const fetchWizardEnv = async () => {
+ try {
+ const response = await fetch('/api/v1/account/wizard-env');
+ const data: WizardEnvType = await response.json();
+ setWizardEnv(data);
+ } catch (error) {
+ console.error('Failed to fetch wizard environment:', error);
+ }
+ };
+
+ fetchWizardEnv();
+ }, []);
+
+ // If Integrations tab is selected but disabled, fallback to General
+ useEffect(() => {
+ if (tab === 'Integrations' && wizardEnv?.DISABLE_INTEGRATIONS === 'true') {
+ setTab('General');
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [wizardEnv?.DISABLE_INTEGRATIONS]);
+
+ return (
+
+
Account
+
+ {wizardEnv != undefined && (
+ <>
+
+ setTab('General')}
+ >
+ General
+
+ setTab('Notifications')}
+ >
+ Notifications
+
+ {wizardEnv.DISABLE_INTEGRATIONS !== 'true' && (
+ setTab('Integrations')}
+ >
+ Integrations
+
+ )}
+
+
+ {tab === 'General' && (
+ <>
+
+
+
+ >
+ )}
+
+ {tab === 'Notifications' && (
+ <>
+
+
+ >
+ )}
+
+ {tab === 'Integrations' && wizardEnv.DISABLE_INTEGRATIONS !== 'true' &&
}
+ >
+ )}
+
+ );
+}
diff --git a/Containers/UserSettings/UsernameSettings/UsernameSettings.js b/Containers/UserSettings/UsernameSettings/UsernameSettings.js
deleted file mode 100644
index 27df35b..0000000
--- a/Containers/UserSettings/UsernameSettings/UsernameSettings.js
+++ /dev/null
@@ -1,147 +0,0 @@
-//Lib
-import { toast } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-import classes from '../UserSettings.module.css';
-import { useState } from 'react';
-import { useForm } from 'react-hook-form';
-import { SpinnerDotted } from 'spinners-react';
-
-//Components
-import Error from '../../../Components/UI/Error/Error';
-import Info from '../../../Components/UI/Info/Info';
-
-export default function UsernameSettings(props) {
- //Var
- const toastOptions = {
- position: 'top-right',
- autoClose: 8000,
- hideProgressBar: false,
- closeOnClick: true,
- pauseOnHover: true,
- draggable: true,
- progress: undefined,
- };
-
- const {
- register,
- handleSubmit,
- reset,
- formState: { errors, isSubmitting, isValid },
- } = useForm({ mode: 'onChange' });
-
- ////State
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState();
- const [info, setInfo] = useState(false);
-
- ////Functions
- //Form submit Handler for ADD a repo
- const formSubmitHandler = async (data) => {
- //Remove old error
- setError();
- //Loading button on submit to avoid multiple send.
- setIsLoading(true);
- //POST API to update the username
- try {
- const response = await fetch('/api/account/updateUsername', {
- method: 'PUT',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify(data),
- });
- const result = await response.json();
-
- if (!response.ok) {
- setIsLoading(false);
- reset();
- setError(result.message);
- setTimeout(() => setError(), 4000);
- } else {
- reset();
- setIsLoading(false);
- setInfo(true);
- toast.success('Username edited !', toastOptions);
- }
- } catch (error) {
- reset();
- setIsLoading(false);
- setError("Can't update your username. Contact your administrator.");
- setTimeout(() => setError(), 4000);
- }
- };
- return (
- <>
- {/* Username */}
-
-
-
Username
-
-
-
- {info ? (
- //For local JWTs (cookie) without an OAuth provider, Next-Auth does not allow
- //at the time this code is written to refresh client-side session information
- //without triggering a logout.
- //I chose to inform the user to reconnect rather than force logout.
-
- ) : (
-
-
- {error && }
-
- {errors.username && (
-
- {errors.username.message}
-
- )}
-
-
- {isLoading ? (
-
- ) : (
- 'Update your username'
- )}
-
-
- )}
-
-
-
- >
- );
-}
diff --git a/Containers/UserSettings/UsernameSettings/UsernameSettings.tsx b/Containers/UserSettings/UsernameSettings/UsernameSettings.tsx
new file mode 100644
index 0000000..4e493fc
--- /dev/null
+++ b/Containers/UserSettings/UsernameSettings/UsernameSettings.tsx
@@ -0,0 +1,123 @@
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { toast, ToastOptions } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { useFormStatus } from '~/hooks';
+import { UsernameSettingDTO } from '~/types';
+import classes from '../UserSettings.module.css';
+
+//Components
+import Info from '~/Components/UI/Info/Info';
+import { useLoader } from '~/contexts/LoaderContext';
+
+export default function UsernameSettings(props: UsernameSettingDTO) {
+ const toastOptions: ToastOptions = {
+ position: 'top-right',
+ autoClose: 8000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ };
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { errors, isSubmitting },
+ } = useForm({ mode: 'onChange' });
+ const { start, stop } = useLoader();
+
+ const { isLoading, setIsLoading } = useFormStatus();
+
+ ////State
+ const [info, setInfo] = useState(false);
+
+ ////Functions
+ const formSubmitHandler = async (data: UsernameSettingDTO) => {
+ start();
+ setIsLoading(true);
+
+ try {
+ const response = await fetch('/api/v1/account/username', {
+ method: 'PUT',
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+ const result = await response.json();
+
+ if (!response.ok) {
+ toast.error(result.message, toastOptions);
+ } else {
+ setInfo(true);
+ toast.success('Username edited !', toastOptions);
+ }
+ } catch (error) {
+ toast.error('Failed to update username. Please try again.', toastOptions);
+ } finally {
+ reset();
+ stop();
+ setIsLoading(false);
+ }
+ };
+ return (
+ <>
+ {/* Username */}
+
+
+
Username
+
+
+
+ {info ? (
+ //For local JWTs (cookie) without an OAuth provider, Next-Auth does not allow
+ //at the time this code is written to refresh client-side session information
+ //without triggering a logout.
+ //I chose to inform the user to reconnect rather than force logout.
+
+ ) : (
+
+
+
+ {errors.username && (
+ {errors.username.message}
+ )}
+
+
+ Update your username
+
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/Dockerfile b/Dockerfile
index 07615dd..3621e8f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,37 +1,47 @@
-FROM node:20-bookworm-slim as base
-
ARG UID=1001
ARG GID=1001
+FROM node:22-bookworm-slim as base
+
# build stage
FROM base AS deps
+RUN corepack enable && corepack prepare pnpm@9 --activate
+
WORKDIR /app
-COPY package.json package-lock.json ./
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
-RUN npm ci --only=production
+RUN pnpm install --frozen-lockfile --prod
FROM base AS builder
+RUN corepack enable && corepack prepare pnpm@9 --activate
+
WORKDIR /app
-COPY --from=deps /app/node_modules ./node_modules
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+
+RUN pnpm install --frozen-lockfile
COPY . .
-RUN sed -i "s/images:/output: 'standalone',images:/" next.config.js
+RUN sed -i "s/images:/output: 'standalone',images:/" next.config.ts
-RUN npm run build
+RUN pnpm run build
# run stage
FROM base AS runner
+ARG UID
+ARG GID
+
ENV NODE_ENV production
+ENV HOSTNAME=
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y \
- supervisor curl jq jc borgbackup/bookworm-backports openssh-server rsyslog && \
+ supervisor curl jq jc borgbackup/bookworm-backports openssh-server && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN groupadd -g ${GID} borgwarehouse && useradd -m -u ${UID} -g ${GID} borgwarehouse
@@ -46,11 +56,10 @@ COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/.next/standalone ./
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/public ./public
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/.next/static ./.next/static
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker/supervisord.conf ./
-COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker/rsyslog.conf /etc/rsyslog.conf
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker/sshd_config ./
USER borgwarehouse
EXPOSE 3000 22
-ENTRYPOINT ["./docker-bw-init.sh"]
\ No newline at end of file
+ENTRYPOINT ["./docker-bw-init.sh"]
diff --git a/README.md b/README.md
index 0441a61..110ef4e 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,19 @@
+[![TypeScript][typescript.js]][typescript-url]
[![Next][Next.js]][Next-url]
[![React][React.js]][React-url]
-[](https://hub.docker.com/r/borgwarehouse/borgwarehouse)
+[](https://hub.docker.com/r/borgwarehouse/borgwarehouse)
+
+
- BorgWarehouse
+
A fast and modern WebUI for a BorgBackup's central repository server.
@@ -20,17 +23,17 @@
## โญ Support the Project
+
-
If you find BorgWarehouse helpful or interesting, please consider **giving it a star on GitHub** and **[sponsoring](https://github.com/sponsors/Ravinou)**. Your support is greatly appreciated!
## โจ What is BorgWarehouse ?
@@ -41,13 +44,14 @@ Today, if you want to have a large server on which you centralize backups of Bor
With BorgWarehouse, you have an interface that allows you to do all this simply and quickly :
-- **add** repositories
-- **edit** existing repositories
-- **delete** repositories
-- be **alerted** if there are no recent backups
-- **monitor** the volume of data
-- **flexibly manage quotas** for each repository
-- ...
+- **add** repositories
+- **edit** existing repositories
+- **delete** repositories
+- be **alerted** if there are no recent backups
+- **monitor** the volume of data
+- **flexibly manage quotas** for each repository
+- manage everything you want through the **REST API**
+- ...
The whole system part is automatically managed by BorgWarehouse and **you don't have to touch your terminal anymore** while enjoying a visual feedback on the status of your repositories.
@@ -70,10 +74,23 @@ Check the online documentation [just here](https://borgwarehouse.com/docs/admin-
## โค๏ธ Special thanks to sponsors โค๏ธ
-
-
-
+### ๐ฅ Current sponsors ๐ฅ
+
+
+
+
+
+
+
+#### Past sponsors
+
+
+
+
+
+[typescript.js]: https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white
+[typescript-url]: https://www.typescriptlang.org/
[next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
[next-url]: https://nextjs.org/
[react.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
diff --git a/contexts/LoaderContext.tsx b/contexts/LoaderContext.tsx
new file mode 100644
index 0000000..c296054
--- /dev/null
+++ b/contexts/LoaderContext.tsx
@@ -0,0 +1,25 @@
+import { createContext, useContext } from 'react';
+import NProgress from 'nprogress';
+
+type LoaderContextType = {
+ start: () => void;
+ stop: () => void;
+};
+
+const LoaderContext = createContext({
+ start: () => {},
+ stop: () => {},
+});
+
+export const LoaderProvider = ({ children }: { children: React.ReactNode }) => {
+ const start = () => NProgress.start();
+ const stop = () => NProgress.done();
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useLoader = () => useContext(LoaderContext);
diff --git a/docker-compose.yml b/docker-compose.yml
index f8f9e1b..f5c051b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,30 +1,27 @@
-version: '3'
services:
- borgwarehouse:
- container_name: borgwarehouse
- # If you want to build the image yourself, uncomment the following lines and comment the image line
- #build:
- # context: .
- # dockerfile: Dockerfile
- # args:
- # - UID=${UID}
- # - GID=${GID}
- image: borgwarehouse/borgwarehouse
- user: '${UID:?UID variable missing}:${GID:?GID variable missing}'
- ports:
- - '${WEB_SERVER_PORT:?WEB_SERVER_PORT variable missing}:3000'
- - '${SSH_SERVER_PORT:?SSH_SERVER_PORT variable missing}:22'
- env_file:
- - .env
- volumes:
- - ${CONFIG_PATH:?CONFIG_PATH variable missing}:/home/borgwarehouse/app/config
- - ${SSH_PATH:?SSH_PATH variable missing}:/home/borgwarehouse/.ssh
- - ${SSH_HOST:?SSH_HOST variable missing}:/etc/ssh
- - ${BORG_REPOSITORY_PATH:?BORG_REPOSITORY_PATH variable missing}:/home/borgwarehouse/repos
- - ${TMP_PATH:?TMP_PATH variable missing}:/home/borgwarehouse/tmp
- - ${LOGS_PATH:?LOGS_PATH variable missing}:/home/borgwarehouse/logs
- #ย Apprise is used to send notifications, it's optional. http://apprise:8000 is the URL to use in BorgWarehouse.
- apprise:
- container_name: apprise
- image: caronc/apprise
- user: 'www-data:www-data'
+ borgwarehouse:
+ container_name: borgwarehouse
+ # If you want to build the image yourself, uncomment the following lines and comment the image line
+ #build:
+ # context: .
+ # dockerfile: Dockerfile
+ # args:
+ # - UID=${UID}
+ # - GID=${GID}
+ image: borgwarehouse/borgwarehouse
+ user: '${UID:?UID variable missing}:${GID:?GID variable missing}'
+ ports:
+ - '${WEB_SERVER_PORT:?WEB_SERVER_PORT variable missing}:3000'
+ - '${SSH_SERVER_PORT:?SSH_SERVER_PORT variable missing}:22'
+ env_file:
+ - .env
+ volumes:
+ - ${CONFIG_PATH:?CONFIG_PATH variable missing}:/home/borgwarehouse/app/config
+ - ${SSH_PATH:?SSH_PATH variable missing}:/home/borgwarehouse/.ssh
+ - ${SSH_HOST:?SSH_HOST variable missing}:/etc/ssh
+ - ${BORG_REPOSITORY_PATH:?BORG_REPOSITORY_PATH variable missing}:/home/borgwarehouse/repos
+ #ย Apprise is used to send notifications, it's optional. http://apprise:8000 is the URL to use in BorgWarehouse.
+ apprise:
+ container_name: apprise
+ image: caronc/apprise
+ user: 'www-data:www-data'
diff --git a/docker/rsyslog.conf b/docker/rsyslog.conf
deleted file mode 100644
index 7ece884..0000000
--- a/docker/rsyslog.conf
+++ /dev/null
@@ -1,40 +0,0 @@
-# rsyslog configuration file
-
-$WorkDirectory /home/borgwarehouse/tmp
-$FileOwner borgwarehouse
-$FileGroup borgwarehouse
-$FileCreateMode 0640
-$DirCreateMode 0755
-$Umask 0022
-
-$RepeatedMsgReduction on
-
-module(load="imfile" PollingInterval="10")
-
-input(type="imfile"
- File="/home/borgwarehouse/tmp/borgwarehouse.log"
- Tag="BorgWarehouse"
- Severity="info"
- Facility="local7"
- ruleset="bwLogs")
-
-input(type="imfile"
- File="/home/borgwarehouse/tmp/sshd.log"
- Tag="sshd"
- Severity="info"
- Facility="local7"
- ruleset="sshdLogs")
-
-$template myFormat,"%timegenerated:::date-rfc3339% %syslogtag% %msg%\n"
-
-ruleset(name="bwLogs") {
- action(type="omfile"
- File="/home/borgwarehouse/logs/borgwarehouse.log"
- Template="myFormat")
-}
-
-ruleset(name="sshdLogs") {
- action(type="omfile"
- File="/home/borgwarehouse/logs/sshd.log"
- Template="myFormat")
-}
diff --git a/docker/supervisord.conf b/docker/supervisord.conf
index 0930b95..ba283f0 100644
--- a/docker/supervisord.conf
+++ b/docker/supervisord.conf
@@ -1,24 +1,21 @@
[supervisord]
nodaemon=true
-logfile=/home/borgwarehouse/logs/supervisord.log
+logfile=/dev/stdout
+logfile_maxbytes=0
loglevel=error
-pidfile=/home/borgwarehouse/tmp/supervisord.pid
-logfile_maxbytes=10MB
-logfile_backups=5
[program:sshd]
command=/usr/sbin/sshd -D -e -f /etc/ssh/sshd_config
-stdout_logfile=/home/borgwarehouse/tmp/sshd.log
-stdout_logfile_maxbytes=10MB
-stdout_logfile_backups=5
-redirect_stderr=true
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+redirect_stderr=false
[program:borgwarehouse]
command=/usr/local/bin/node server.js
-stdout_logfile=/home/borgwarehouse/tmp/borgwarehouse.log
-stdout_logfile_maxbytes=10MB
-stdout_logfile_backups=5
-redirect_stderr=true
-
-[program:rsyslogd]
-command=rsyslogd -n -i /home/borgwarehouse/tmp/rsyslogd.pid -f /etc/rsyslog.conf
\ No newline at end of file
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+redirect_stderr=false
\ No newline at end of file
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..2044e89
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,16 @@
+import { defineConfig, globalIgnores } from 'eslint/config';
+import nextVitals from 'eslint-config-next/core-web-vitals';
+
+const eslintConfig = defineConfig([
+ ...nextVitals,
+ // Override default ignores of eslint-config-next.
+ globalIgnores([
+ // Default ignores of eslint-config-next:
+ '.next/**',
+ 'out/**',
+ 'build/**',
+ 'next-env.d.ts',
+ ]),
+]);
+
+export default eslintConfig;
diff --git a/helpers/functions/__mocks__/apiResponse.ts b/helpers/functions/__mocks__/apiResponse.ts
new file mode 100644
index 0000000..5903d44
--- /dev/null
+++ b/helpers/functions/__mocks__/apiResponse.ts
@@ -0,0 +1,12 @@
+const ApiResponse = {
+ success: vi.fn(),
+ badRequest: vi.fn(),
+ unauthorized: vi.fn(),
+ forbidden: vi.fn(),
+ notFound: vi.fn(),
+ methodNotAllowed: vi.fn(),
+ validationError: vi.fn(),
+ serverError: vi.fn(),
+};
+
+export default ApiResponse;
diff --git a/helpers/functions/apiResponse.ts b/helpers/functions/apiResponse.ts
new file mode 100644
index 0000000..2382f5c
--- /dev/null
+++ b/helpers/functions/apiResponse.ts
@@ -0,0 +1,71 @@
+import { NextApiResponse } from 'next';
+
+const getErrorMessage = (error: unknown): any => {
+ if (error instanceof Error) {
+ const shellError = error as any;
+
+ // Handle shell errors
+ if ('code' in shellError || 'stderr' in shellError || 'stdout' in shellError) {
+ return {
+ code: shellError.code ?? null,
+ cmd: shellError.cmd ?? null,
+ stderr: shellError.stderr ?? null,
+ stdout: shellError.stdout ?? null,
+ };
+ }
+
+ return error.message;
+ }
+
+ if (typeof error === 'object' && error !== null && 'code' in error) {
+ const err = error as { code?: string };
+ if (err.code === 'ENOENT') {
+ return 'No such file or directory';
+ }
+ }
+
+ return 'API error, contact the administrator';
+};
+
+export default class ApiResponse {
+ static success(res: NextApiResponse, message = 'Success', data?: T) {
+ res.status(200).json({ status: 200, message, data });
+ }
+
+ static badRequest(res: NextApiResponse, message = 'Bad Request') {
+ res.status(400).json({ status: 400, message });
+ }
+
+ static unauthorized(res: NextApiResponse, message = 'Unauthorized') {
+ res.status(401).json({ status: 401, message });
+ }
+
+ static forbidden(res: NextApiResponse, message = 'Forbidden') {
+ res.status(403).json({ status: 403, message });
+ }
+
+ static notFound(res: NextApiResponse, message = 'Not Found') {
+ res.status(404).json({ status: 404, message });
+ }
+
+ static methodNotAllowed(res: NextApiResponse, message = 'Method Not Allowed') {
+ res.status(405).json({ status: 405, message });
+ }
+
+ static validationError(res: NextApiResponse, message = 'Validation Error') {
+ res.status(422).json({ status: 422, message });
+ }
+
+ static conflict(res: NextApiResponse, message = 'Conflict') {
+ res.status(409).json({ status: 409, message });
+ }
+
+ static serverError(
+ res: NextApiResponse,
+ error: unknown,
+ fallbackMessage = 'API error, contact the administrator'
+ ) {
+ const message = getErrorMessage(error) || fallbackMessage;
+ res.status(500).json({ status: 500, message });
+ }
+}
diff --git a/helpers/functions/auth.js b/helpers/functions/auth.js
deleted file mode 100644
index 9c39a0f..0000000
--- a/helpers/functions/auth.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// This function is used to hash user passwords and to verify them with the bcryptjs library
-//Lib
-import { hash, compare } from 'bcryptjs';
-
-export async function hashPassword(password) {
- return await hash(password, 12);
-}
-
-export async function verifyPassword(password, hashedPassword) {
- return await compare(password, hashedPassword);
-}
diff --git a/helpers/functions/index.ts b/helpers/functions/index.ts
new file mode 100644
index 0000000..b0b3228
--- /dev/null
+++ b/helpers/functions/index.ts
@@ -0,0 +1,4 @@
+import lanCommandOption from './lanCommandOption';
+import isSshPubKeyDuplicate from './isSshPubKeyDuplicate';
+
+export { lanCommandOption, isSshPubKeyDuplicate };
diff --git a/helpers/functions/isSshPubKeyDuplicate.test.ts b/helpers/functions/isSshPubKeyDuplicate.test.ts
new file mode 100644
index 0000000..674b58e
--- /dev/null
+++ b/helpers/functions/isSshPubKeyDuplicate.test.ts
@@ -0,0 +1,70 @@
+import { describe, it, expect } from 'vitest';
+import isSshPubKeyDuplicate from './isSshPubKeyDuplicate';
+import { Optional, Repository } from '~/types';
+
+describe('isSshPubKeyDuplicate', () => {
+ it('should return true if the SSH public key is duplicated', () => {
+ const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
+ const repoList: Array> = [
+ { sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
+ ];
+
+ expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(true);
+ });
+
+ it('should return false if the SSH public key is not duplicated', () => {
+ const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAdifferentkey user@hostname';
+ const repoList: Array> = [
+ { sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
+ ];
+
+ expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(false);
+ });
+
+ it('should throw an error if pubKey is missing', () => {
+ const repoList: Array> = [
+ { sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
+ ];
+
+ expect(() => isSshPubKeyDuplicate('', repoList)).toThrow(
+ 'Missing or invalid parameters for duplicate SSH public key check.'
+ );
+ });
+
+ it('should throw an error if repoList is missing', () => {
+ const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
+
+ expect(() => isSshPubKeyDuplicate(pubKey, null as any)).toThrow(
+ 'Missing or invalid parameters for duplicate SSH public key check.'
+ );
+ });
+
+ it('should return false if repoList is empty', () => {
+ const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
+ const repoList: Array> = [];
+
+ expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(false);
+ });
+
+ it('should handle repositories with undefined sshPublicKey', () => {
+ const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
+ const repoList: Array> = [
+ // @ts-expect-error
+ { sshPublicKey: undefined } as Repository,
+ { sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
+ ];
+
+ expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(true);
+ });
+
+ it('should handle repositories with null sshPublicKey', () => {
+ const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
+ const repoList: Array> = [
+ // @ts-expect-error
+ { sshPublicKey: null } as Repository,
+ { sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAdifferentkey other@host' } as Repository,
+ ];
+
+ expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(false);
+ });
+});
diff --git a/helpers/functions/isSshPubKeyDuplicate.ts b/helpers/functions/isSshPubKeyDuplicate.ts
new file mode 100644
index 0000000..7468017
--- /dev/null
+++ b/helpers/functions/isSshPubKeyDuplicate.ts
@@ -0,0 +1,27 @@
+import { Optional, Repository } from '~/types';
+
+/**
+ * Checks if the given SSH public key is duplicated in the provided repository list by removing the comment part.
+ *
+ * @param {string} pubKey - The SSH public key to check.
+ * @param {Array} repoList - The list of repositories with their SSH public keys.
+ * @returns {boolean} - Returns true if the SSH public key is duplicated, otherwise false.
+ * @throws {Error} - Throws an error if required parameters are missing or invalid.
+ */
+export default function isSshPubKeyDuplicate(
+ pubKey: string,
+ repoList: Array>
+): boolean {
+ if (!pubKey || !repoList || !Array.isArray(repoList)) {
+ throw new Error('Missing or invalid parameters for duplicate SSH public key check.');
+ }
+
+ // Compare the key part only by removing the comment
+ const pubKeyWithoutComment = pubKey.split(' ').slice(0, 2).join(' ');
+
+ // Check if the normalized key is already in the repository list
+ return repoList.some((repo) => {
+ const repoSshKeyWithoutComment = repo?.sshPublicKey?.split(' ').slice(0, 2).join(' ');
+ return repoSshKeyWithoutComment === pubKeyWithoutComment;
+ });
+}
diff --git a/helpers/functions/lanCommandOption.js b/helpers/functions/lanCommandOption.js
deleted file mode 100644
index 40918e0..0000000
--- a/helpers/functions/lanCommandOption.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default function lanCommandOption(wizardEnv, lanCommand) {
- let FQDN;
- let SSH_SERVER_PORT;
- if (lanCommand && wizardEnv.FQDN_LAN && wizardEnv.SSH_SERVER_PORT_LAN) {
- FQDN = wizardEnv.FQDN_LAN;
- SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT_LAN;
- } else {
- FQDN = wizardEnv.FQDN;
- SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT;
- }
-
- return { FQDN, SSH_SERVER_PORT };
-}
diff --git a/helpers/functions/lanCommandOption.test.ts b/helpers/functions/lanCommandOption.test.ts
new file mode 100644
index 0000000..d4dbdd3
--- /dev/null
+++ b/helpers/functions/lanCommandOption.test.ts
@@ -0,0 +1,75 @@
+import { describe, it, expect } from 'vitest';
+import lanCommandOption from './lanCommandOption';
+import { WizardEnvType } from '~/types';
+
+describe('lanCommandOption', () => {
+ it('should return undefined values when wizardEnv is not provided', () => {
+ const result = lanCommandOption();
+ expect(result).toEqual({ FQDN: undefined, SSH_SERVER_PORT: undefined });
+ });
+
+ it('should return FQDN and SSH_SERVER_PORT from wizardEnv when lanCommand is false', () => {
+ const wizardEnv: Partial = {
+ FQDN: 'example.com',
+ FQDN_LAN: 'lan.example.com',
+ SSH_SERVER_PORT: '22',
+ SSH_SERVER_PORT_LAN: '2222',
+ HIDE_SSH_PORT: 'false',
+ };
+
+ const result = lanCommandOption(wizardEnv, false);
+ expect(result).toEqual({ FQDN: 'example.com', SSH_SERVER_PORT: ':22' });
+ });
+
+ it('should return FQDN_LAN and SSH_SERVER_PORT_LAN from wizardEnv when lanCommand is true', () => {
+ const wizardEnv: Partial = {
+ FQDN: 'example.com',
+ FQDN_LAN: 'lan.example.com',
+ SSH_SERVER_PORT: '22',
+ SSH_SERVER_PORT_LAN: '2222',
+ HIDE_SSH_PORT: 'false',
+ };
+
+ const result = lanCommandOption(wizardEnv, true);
+ expect(result).toEqual({ FQDN: 'lan.example.com', SSH_SERVER_PORT: ':2222' });
+ });
+
+ it('should return undefined for SSH_SERVER_PORT when HIDE_SSH_PORT is true', () => {
+ const wizardEnv: Partial = {
+ FQDN: 'example.com',
+ FQDN_LAN: 'lan.example.com',
+ SSH_SERVER_PORT: '22',
+ SSH_SERVER_PORT_LAN: '2222',
+ HIDE_SSH_PORT: 'true',
+ };
+
+ const result = lanCommandOption(wizardEnv, false);
+ expect(result).toEqual({ FQDN: 'example.com', SSH_SERVER_PORT: undefined });
+ });
+
+ it('should fallback to FQDN and should leave ssh server port to undefined for some usages', () => {
+ const wizardEnv: Partial = {
+ FQDN: 'example.com',
+ FQDN_LAN: undefined,
+ SSH_SERVER_PORT: '22',
+ SSH_SERVER_PORT_LAN: undefined,
+ HIDE_SSH_PORT: 'false',
+ };
+
+ const result = lanCommandOption(wizardEnv, true);
+ expect(result).toEqual({ FQDN: 'example.com', SSH_SERVER_PORT: undefined });
+ });
+
+ it('should handle missing FQDN and SSH_SERVER_PORT gracefully', () => {
+ const wizardEnv: Partial = {
+ FQDN: undefined,
+ FQDN_LAN: 'lan.example.com',
+ SSH_SERVER_PORT: undefined,
+ SSH_SERVER_PORT_LAN: '2222',
+ HIDE_SSH_PORT: 'false',
+ };
+
+ const result = lanCommandOption(wizardEnv, false);
+ expect(result).toEqual({ FQDN: undefined, SSH_SERVER_PORT: undefined });
+ });
+});
diff --git a/helpers/functions/lanCommandOption.ts b/helpers/functions/lanCommandOption.ts
new file mode 100644
index 0000000..43b1424
--- /dev/null
+++ b/helpers/functions/lanCommandOption.ts
@@ -0,0 +1,24 @@
+import { Optional, WizardEnvType } from '~/types';
+
+export default function lanCommandOption(
+ wizardEnv?: Partial,
+ lanCommand?: boolean
+): { FQDN: Optional; SSH_SERVER_PORT: Optional } {
+ if (!wizardEnv) {
+ return { FQDN: undefined, SSH_SERVER_PORT: undefined };
+ }
+
+ const { FQDN, FQDN_LAN, SSH_SERVER_PORT, SSH_SERVER_PORT_LAN, HIDE_SSH_PORT } = wizardEnv;
+
+ const isPortHidden = HIDE_SSH_PORT === 'true';
+
+ const selectedFQDN = lanCommand && FQDN_LAN ? FQDN_LAN : FQDN;
+ const selectedPort = lanCommand ? SSH_SERVER_PORT_LAN : SSH_SERVER_PORT;
+
+ const formattedPort = !isPortHidden && selectedPort ? `:${selectedPort}` : undefined;
+
+ return {
+ FQDN: selectedFQDN,
+ SSH_SERVER_PORT: formattedPort,
+ };
+}
diff --git a/helpers/functions/nodemailerSMTP.js b/helpers/functions/nodemailerSMTP.js
deleted file mode 100644
index 3c186b8..0000000
--- a/helpers/functions/nodemailerSMTP.js
+++ /dev/null
@@ -1,18 +0,0 @@
-//Lib
-import nodemailer from 'nodemailer';
-
-export default function nodemailerSMTP() {
- const transporter = nodemailer.createTransport({
- port: process.env.MAIL_SMTP_PORT,
- host: process.env.MAIL_SMTP_HOST,
- auth: {
- user: process.env.MAIL_SMTP_LOGIN,
- pass: process.env.MAIL_SMTP_PWD,
- },
- tls: {
- // do not fail on invalid certs >> allow self-signed or invalid TLS certificate
- rejectUnauthorized: process.env.MAIL_REJECT_SELFSIGNED_TLS,
- },
- });
- return transporter;
-}
diff --git a/helpers/functions/repoHistory.js b/helpers/functions/repoHistory.js
deleted file mode 100644
index e9bfc55..0000000
--- a/helpers/functions/repoHistory.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { promises as fs } from 'fs';
-import path from 'path';
-
-export default async function repoHistory(data) {
- try {
- const repoHistoryDir = path.join(process.cwd(), '/config/versions');
- const maxBackupCount = parseInt(process.env.MAX_REPO_BACKUP_COUNT) || 8;
- const timestamp = new Date().toISOString();
- const backupDate = timestamp.split('T')[0];
-
- //Create the directory if it does not exist
- await fs.mkdir(repoHistoryDir, { recursive: true });
-
- const existingBackups = await fs.readdir(repoHistoryDir);
-
- if (existingBackups.length >= maxBackupCount) {
- existingBackups.sort();
- const backupsToDelete = existingBackups.slice(
- 0,
- existingBackups.length - maxBackupCount + 1
- );
- for (const backupToDelete of backupsToDelete) {
- const backupFilePathToDelete = path.join(
- repoHistoryDir,
- backupToDelete
- );
- await fs.unlink(backupFilePathToDelete);
- }
- }
-
- const backupFileName = `${backupDate}.log`;
- const backupFilePath = path.join(repoHistoryDir, backupFileName);
- const jsonData = JSON.stringify(data, null, 2);
-
- const logData = `\n>>>> History of file repo.json at "${timestamp}" <<<<\n${jsonData}\n`;
-
- // รcrire ou rรฉรฉcrire le fichier avec le contenu mis ร jour
- await fs.appendFile(backupFilePath, logData);
- } catch (error) {
- console.error(
- 'An error occurred while saving the repo history :',
- error.message
- );
- }
-}
diff --git a/helpers/functions/repositoryNameCheck.test.ts b/helpers/functions/repositoryNameCheck.test.ts
new file mode 100644
index 0000000..b767211
--- /dev/null
+++ b/helpers/functions/repositoryNameCheck.test.ts
@@ -0,0 +1,52 @@
+import { describe, it, expect } from 'vitest';
+import repositoryNameCheck from './repositoryNameCheck';
+
+describe('repositoryNameCheck', () => {
+ it('should return true for a valid 8-character hexadecimal string', () => {
+ expect(repositoryNameCheck('a1b2c3d4')).toBe(true);
+ });
+
+ it('should return false for a string shorter than 8 characters', () => {
+ expect(repositoryNameCheck('a1b2c3')).toBe(false);
+ });
+
+ it('should return false for a string longer than 8 characters', () => {
+ expect(repositoryNameCheck('a1b2c3d4e5')).toBe(false);
+ });
+
+ it('should return false for a string with non-hexadecimal characters', () => {
+ expect(repositoryNameCheck('a1b2c3g4')).toBe(false);
+ });
+
+ it('should return false for an empty string', () => {
+ expect(repositoryNameCheck('')).toBe(false);
+ });
+
+ it('should return false for a string with special characters', () => {
+ expect(repositoryNameCheck('a1b2c3d@')).toBe(false);
+ });
+
+ it('should return false for a string with uppercase hexadecimal characters', () => {
+ expect(repositoryNameCheck('A1B2C3D4')).toBe(false);
+ });
+
+ it('should return false for a string with spaces', () => {
+ expect(repositoryNameCheck('a1b2 c3d4')).toBe(false);
+ });
+
+ it('should return false for a non string name', () => {
+ expect(repositoryNameCheck(12345678)).toBe(false);
+ });
+
+ it('should return false for null', () => {
+ expect(repositoryNameCheck(null)).toBe(false);
+ });
+
+ it('should return false for undefined', () => {
+ expect(repositoryNameCheck(undefined)).toBe(false);
+ });
+
+ it('should return false for boolean', () => {
+ expect(repositoryNameCheck(true)).toBe(false);
+ });
+});
diff --git a/helpers/functions/repositoryNameCheck.ts b/helpers/functions/repositoryNameCheck.ts
new file mode 100644
index 0000000..de28565
--- /dev/null
+++ b/helpers/functions/repositoryNameCheck.ts
@@ -0,0 +1,9 @@
+// BorgWarehouse repository name is an 8-character hexadecimal string
+
+export default function repositoryNameCheck(name: unknown): boolean {
+ if (typeof name !== 'string') {
+ return false;
+ }
+ const repositoryNameRegex = /^[a-f0-9]{8}$/;
+ return repositoryNameRegex.test(name) ? true : false;
+}
diff --git a/helpers/functions/timestampConverter.js b/helpers/functions/timestampConverter.js
deleted file mode 100644
index b943860..0000000
--- a/helpers/functions/timestampConverter.js
+++ /dev/null
@@ -1,12 +0,0 @@
-// This function is used to parse the date and time into a human readable format from the timestamp.
-export default function timestampConverter(UNIX_timestamp) {
- const a = new Date(UNIX_timestamp * 1000);
- const year = a.getFullYear();
- const month = a.getMonth() + 1;
- const date = a.getDate();
- const hour = a.getHours();
- const min = (a.getMinutes() < 10 ? '0' : '') + a.getMinutes();
- //const sec = a.getSeconds();
- const time = year + '/' + month + '/' + date + ' ' + hour + ':' + min;
- return time;
-}
diff --git a/helpers/shells/createRepo.sh b/helpers/shells/createRepo.sh
index 00cc876..6df7824 100755
--- a/helpers/shells/createRepo.sh
+++ b/helpers/shells/createRepo.sh
@@ -31,8 +31,8 @@ pool="${home}/repos"
authorized_keys="${home}/.ssh/authorized_keys"
# Check args
-if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" != "true" ] && [ "$3" != "false" ];then
- echo -n "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]"
+if [ "$1" == "" ] || [ "$2" == "" ] || ! [[ "$2" =~ ^[0-9]+$ ]] || [ "$3" != "true" ] && [ "$3" != "false" ]; then
+ echo -n "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]" >&2
exit 1
fi
@@ -41,25 +41,25 @@ fi
pattern='(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?'
if [[ ! "$1" =~ $pattern ]]
then
- echo -n "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)"
+ echo -n "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)" >&2
exit 2
fi
## Check if authorized_keys exists
if [ ! -f "${authorized_keys}" ];then
- echo -n "${authorized_keys} must be present"
+ echo -n "${authorized_keys} must be present" >&2
exit 5
fi
# Check if SSH pub key is already present in authorized_keys
if grep -q "$1" "$authorized_keys"; then
- echo -n "SSH pub key already present in authorized_keys"
+ echo -n "SSH pub key already present in authorized_keys" >&2
exit 3
fi
# Check if borgbackup is installed
if ! [ -x "$(command -v borg)" ]; then
- echo -n "You must install borgbackup package."
+ echo -n "You must install borgbackup package." >&2
exit 4
fi
@@ -77,7 +77,7 @@ else
fi
## Add ssh public key in authorized_keys with borg restriction for only 1 repository and storage quota
-restricted_authkeys="command=\"cd ${pool};borg serve${appendOnlyMode} --restrict-to-path ${pool}/${repositoryName} --storage-quota $2G\",restrict $1"
+restricted_authkeys="command=\"cd ${pool};borg serve${appendOnlyMode} --restrict-to-repository ${pool}/${repositoryName} --storage-quota $2G\",restrict $1"
echo "$restricted_authkeys" | tee -a "${authorized_keys}" >/dev/null
## Return the repositoryName
diff --git a/helpers/shells/deleteRepo.sh b/helpers/shells/deleteRepo.sh
index c725908..162d533 100755
--- a/helpers/shells/deleteRepo.sh
+++ b/helpers/shells/deleteRepo.sh
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
+### DEPRECATED ### NodeJS will handle this in the future.
+
# Shell created by Raven for BorgWarehouse.
# This shell takes 1 arg : [repositoryName] with 8 char. length only.
# This shell **delete the repository** in arg and **all his data** and the line associated in the authorized_keys file.
@@ -21,16 +23,16 @@ authorized_keys="${home}/.ssh/authorized_keys"
# Check arg
if [[ $# -ne 1 || $1 = "" ]]; then
- echo -n "You must provide a repositoryName in argument."
+ echo -n "You must provide a repositoryName in argument." >&2
exit 1
fi
-# Check if the repositoryName length is 8 char. With createRepo.sh our randoms have a length of 8 characters.
-# If we receive another length there is necessarily a problem.
+# Check if the repositoryName pattern is an hexa 8 char. With createRepo.sh our randoms are hexa of 8 characters.
+# If we receive another pattern there is necessarily a problem.
repositoryName=$1
-if [ ${#repositoryName} != 8 ]; then
- echo -n "Error with the length of the repositoryName."
- exit 2
+if ! [[ "$repositoryName" =~ ^[a-f0-9]{8}$ ]]; then
+ echo "Invalid repository name. Must be an 8-character hex string." >&2
+ exit 2
fi
# Delete the repository and the line associated in the authorized_keys file
diff --git a/helpers/shells/getLastSave.sh b/helpers/shells/getLastSave.sh
index 454ce03..49b0502 100755
--- a/helpers/shells/getLastSave.sh
+++ b/helpers/shells/getLastSave.sh
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
+### DEPRECATED ### NodeJS will handle this in the future.
+
# Shell created by Raven for BorgWarehouse.
# Get the timestamp of the last modification of the file integrity.* for of all repositories in a JSON output.
# stdout will be an array like :
diff --git a/helpers/shells/getStorageUsed.sh b/helpers/shells/getStorageUsed.sh
index 682413f..eb71717 100755
--- a/helpers/shells/getStorageUsed.sh
+++ b/helpers/shells/getStorageUsed.sh
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
+### DEPRECATED ### NodeJS will handle this in the future.
+
# Shell created by Raven for BorgWarehouse.
# Get the size of all repositories in a JSON output.
# stdout will be an array like :
@@ -14,6 +16,9 @@
# Exit when any command fails
set -e
+# Ignore "lost+found" directories
+GLOBIGNORE="LOST+FOUND:lost+found"
+
# Load .env if exists
if [[ -f .env ]]; then
source .env
@@ -22,6 +27,12 @@ fi
# Default value if .env not exists
: "${home:=/home/borgwarehouse}"
-# Use jc to output a JSON format with du command
+# Get the size of each repository and format as JSON
cd "${home}"/repos
-du -s -- * | jc --du
+output=$(du -s -L -- * 2>/dev/null | awk '{print "{\"size\":" $1 ",\"name\":\"" $2 "\"}"}' | jq -s '.')
+if [ -z "$output" ]; then
+ output="[]"
+fi
+
+# Print the JSON output
+echo "$output"
diff --git a/helpers/shells/updateRepo.sh b/helpers/shells/updateRepo.sh
index 958978a..4bfd14d 100755
--- a/helpers/shells/updateRepo.sh
+++ b/helpers/shells/updateRepo.sh
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
+### DEPRECATED ### NodeJS will handle this in the future.
+
# Shell created by Raven for BorgWarehouse.
# This shell takes 4 args: [repositoryName] [new SSH pub key] [quota] [append-only mode (boolean)]
# This shell updates the SSH key and the quota for a repository.
@@ -17,7 +19,7 @@ fi
# Check args
if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ] || [ "$4" != "true" ] && [ "$4" != "false" ]; then
- echo -n "This shell takes 4 args: [repositoryName] [new SSH pub key] [quota] [Append only mode [true|false]]"
+ echo -n "This shell takes 4 args: [repositoryName] [new SSH pub key] [quota] [Append only mode [true|false]]" >&2
exit 1
fi
@@ -26,21 +28,21 @@ fi
pattern='(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?'
if [[ ! "$2" =~ $pattern ]]
then
- echo -n "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)"
+ echo -n "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)" >&2
exit 2
fi
-# Check if repositoryName length is 8 char. With createRepo.sh our randoms have a length of 8 characters.
-# If we receive another length, there is necessarily a problem.
+# Check if the repositoryName pattern is an hexa 8 char. With createRepo.sh our randoms are hexa of 8 characters.
+# If we receive another pattern there is necessarily a problem.
repositoryName=$1
-if [ ${#repositoryName} != 8 ]; then
- echo -n "Error with the length of the repositoryName."
+if ! [[ "$repositoryName" =~ ^[a-f0-9]{8}$ ]]; then
+ echo "Invalid repository name. Must be an 8-character hex string." >&2
exit 3
fi
# Check if a line in authorized_keys contains repository_name
if ! grep -q "command=\".*${repositoryName}.*\",restrict" "$home/.ssh/authorized_keys"; then
- echo -n "No line containing $repositoryName found in authorized_keys"
+ echo -n "No line containing $repositoryName found in authorized_keys" >&2
exit 4
fi
@@ -64,7 +66,7 @@ while IFS= read -r line; do
fi
done < "$home/.ssh/authorized_keys"
if [ "$found" = true ]; then
- echo -n "This SSH pub key is already present in authorized_keys on a different line."
+ echo -n "This SSH pub key is already present in authorized_keys on a different line." >&2
exit 5
fi
diff --git a/helpers/templates/attachments/alert-icon.png b/helpers/templates/attachments/alert-icon.png
new file mode 100644
index 0000000..082898a
Binary files /dev/null and b/helpers/templates/attachments/alert-icon.png differ
diff --git a/helpers/templates/attachments/valid-icon.png b/helpers/templates/attachments/valid-icon.png
new file mode 100644
index 0000000..b0ad696
Binary files /dev/null and b/helpers/templates/attachments/valid-icon.png differ
diff --git a/helpers/templates/emailAlertStatus.js b/helpers/templates/emailAlertStatus.js
deleted file mode 100644
index 8f3d31a..0000000
--- a/helpers/templates/emailAlertStatus.js
+++ /dev/null
@@ -1,166 +0,0 @@
-export default function emailTest(mailTo, username, aliasList) {
- const aliasTemplate = (x) => {
- let str = '';
- for (const alias of x) {
- str = str + '' + alias + ' ';
- }
- return str;
- };
-
- const template = {
- from: 'BorgWarehouse' + '<' + process.env.MAIL_SMTP_FROM + '>',
- to: mailTo,
- subject: 'Down status alert !',
- text: 'Some repositories need attention ! Please, check your BorgWarehouse interface.',
- html:
- `
-
-
-
-
-
- BorgWarehouse
-
-
-
-
-
Some repositories need attention, ` +
- username +
- ` !
-
-
-
-
- List of repositories with down status :
-
-
` +
- aliasTemplate(aliasList) +
- `
-
-
-
-
-
- ๐ฉ
-
-
- Please remember that the status is based on
-
the last modification . Backups are
-
encrypted from end to end between your client and the
- server
- controlled by BorgWarehouse. Don't forget to
-
check the integrity of your backups regularly .
-
-
-
-
-
-`,
- };
- return template;
-}
diff --git a/helpers/templates/emailAlertStatus.ts b/helpers/templates/emailAlertStatus.ts
new file mode 100644
index 0000000..c064d72
--- /dev/null
+++ b/helpers/templates/emailAlertStatus.ts
@@ -0,0 +1,137 @@
+import path from 'path';
+
+export default function emailTest(mailTo: string, username: string, aliasList: string[]) {
+ const aliasTemplate = (x: string[]) => {
+ let str = '';
+ for (const alias of x) {
+ str = str + '' + alias + ' ';
+ }
+ return str;
+ };
+
+ const template = {
+ from: 'BorgWarehouse' + '<' + process.env.MAIL_SMTP_FROM + '>',
+ to: mailTo,
+ subject: 'Down status alert !',
+ text: 'Some repositories require your attention ! Please, check your BorgWarehouse interface.',
+ html:
+ `
+
+
+
+
+
+
+
+
+
+
+
+
BorgWarehouse
+
+
+
+
+
Some repositories require your attention, ` +
+ username +
+ `!
+
+
+
List of repositories with down status:
+
` +
+ aliasTemplate(aliasList) +
+ `
+
+
+
๐ฉ
+
+ Please remember that the status is based on
the last modification . Backups are
encrypted from end to end between your client and the server controlled by BorgWarehouse. Don't forget to
check the integrity of your backups regularly .
+
+
+
+
+
+
+
+ `,
+ attachments: [
+ {
+ path: path.join(process.cwd(), 'helpers/templates/attachments/alert-icon.png'),
+ cid: 'alert-icon',
+ },
+ ],
+ };
+ return template;
+}
diff --git a/helpers/templates/emailTest.js b/helpers/templates/emailTest.js
deleted file mode 100644
index d0dcbc8..0000000
--- a/helpers/templates/emailTest.js
+++ /dev/null
@@ -1,82 +0,0 @@
-export default function emailTest(mailTo, username) {
- const template = {
- from: 'BorgWarehouse' + '<' + process.env.MAIL_SMTP_FROM + '>',
- to: mailTo,
- subject: 'Testing email settings',
- text: 'If you received this email then the mail configuration seems to be correct.',
- html:
- `
-
-
-
-
-
- BorgWarehouse
-
-
-
-
Good job, ` +
- username +
- ` !
-
-
If you received this mail then the configuration seems to be correct.
-
-
-
-
-
-
- `,
- };
- return template;
-}
diff --git a/helpers/templates/emailTest.ts b/helpers/templates/emailTest.ts
new file mode 100644
index 0000000..db8c3d5
--- /dev/null
+++ b/helpers/templates/emailTest.ts
@@ -0,0 +1,105 @@
+import path from 'path';
+
+export default function emailTest(mailTo: string, username: string) {
+ const template = {
+ from: 'BorgWarehouse' + '<' + process.env.MAIL_SMTP_FROM + '>',
+ to: mailTo,
+ subject: 'Testing email settings',
+ text: 'If you received this email then the mail configuration seems to be correct.',
+ html:
+ `
+
+
+
+
+
+
+
+
+
+
+
+
BorgWarehouse
+
+
+
+
+
Good job, ` +
+ username +
+ `!
+
+
+
If you received this mail then the configuration seems to be correct.
+
+
+
+
+
+
+ `,
+ attachments: [
+ {
+ path: path.join(process.cwd(), 'helpers/templates/attachments/valid-icon.png'),
+ cid: 'valid-icon',
+ },
+ ],
+ };
+ return template;
+}
diff --git a/hooks/index.ts b/hooks/index.ts
new file mode 100644
index 0000000..f34857f
--- /dev/null
+++ b/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useFormStatus';
diff --git a/hooks/useFormStatus.ts b/hooks/useFormStatus.ts
new file mode 100644
index 0000000..ffcef3f
--- /dev/null
+++ b/hooks/useFormStatus.ts
@@ -0,0 +1,32 @@
+import { useState } from 'react';
+import { Optional } from '~/types';
+
+export function useFormStatus() {
+ const [isLoading, setIsLoading] = useState(false);
+ const [isSaved, setIsSaved] = useState(false);
+ const [error, setError] = useState>(undefined);
+
+ const handleSuccess = () => {
+ setIsLoading(false);
+ setIsSaved(true);
+ setTimeout(() => setIsSaved(false), 3000);
+ };
+
+ const handleError = (message: string) => {
+ setIsLoading(false);
+ setError(message);
+ setTimeout(() => setError(undefined), 4000);
+ };
+
+ const clearError = () => setError(undefined);
+
+ return {
+ isLoading,
+ isSaved,
+ error,
+ setIsLoading,
+ handleSuccess,
+ handleError,
+ clearError,
+ };
+}
diff --git a/medias/borgwarehouse-og.jpg b/medias/borgwarehouse-og.jpg
new file mode 100644
index 0000000..53eaa35
Binary files /dev/null and b/medias/borgwarehouse-og.jpg differ
diff --git a/medias/borgwarehouse-og.png b/medias/borgwarehouse-og.png
deleted file mode 100644
index 9d9d72d..0000000
Binary files a/medias/borgwarehouse-og.png and /dev/null differ
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 0000000..7996d35
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/dev/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
diff --git a/next.config.js b/next.config.js
deleted file mode 100644
index 7df1b2e..0000000
--- a/next.config.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/** @type {import('next').NextConfig} */
-
-module.exports = {
- // nextConfig
- images: {
- unoptimized: true,
- },
- reactStrictMode: false,
- swcMinify: true,
- //basePath: '/borgwarehouse-demo',
- async redirects() {
- return [
- {
- source: '/setup-wizard',
- destination: '/setup-wizard/1',
- permanent: true,
- },
- {
- source: '/manage-repo',
- destination: '/',
- permanent: true,
- },
- ];
- },
-};
diff --git a/next.config.ts b/next.config.ts
new file mode 100644
index 0000000..b0d8d61
--- /dev/null
+++ b/next.config.ts
@@ -0,0 +1,24 @@
+import type { NextConfig } from 'next';
+
+const nextConfig: NextConfig = {
+ images: {
+ unoptimized: true,
+ },
+ reactStrictMode: false,
+ async redirects() {
+ return [
+ {
+ source: '/setup-wizard',
+ destination: '/setup-wizard/1',
+ permanent: true,
+ },
+ {
+ source: '/manage-repo',
+ destination: '/',
+ permanent: true,
+ },
+ ];
+ },
+};
+
+export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index 1e645f5..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,5151 +0,0 @@
-{
- "name": "borgwarehouse",
- "version": "2.3.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "borgwarehouse",
- "version": "2.3.0",
- "dependencies": {
- "@tabler/icons-react": "^3.3.0",
- "bcryptjs": "^2.4.3",
- "chart.js": "^4.4.2",
- "next": "^14.2.3",
- "next-auth": "^4.24.7",
- "nodemailer": "^6.9.13",
- "react": "^18.3.1",
- "react-chartjs-2": "^5.2.0",
- "react-dom": "^18.3.1",
- "react-hook-form": "^7.51.4",
- "react-select": "^5.8.0",
- "react-toastify": "^10.0.5",
- "spinners-react": "^1.0.7",
- "swr": "^2.2.5"
- },
- "devDependencies": {
- "eslint-config-next": "^14.2.3",
- "prettier": "^3.2.5"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.24.2",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
- "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
- "dependencies": {
- "@babel/highlight": "^7.24.2",
- "picocolors": "^1.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.24.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
- "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
- "dependencies": {
- "@babel/types": "^7.24.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
- "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.24.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
- "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/highlight": {
- "version": "7.24.5",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz",
- "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.24.5",
- "chalk": "^2.4.2",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/highlight/node_modules/ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/highlight/node_modules/chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/highlight/node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/@babel/highlight/node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
- },
- "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/@babel/highlight/node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/highlight/node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/runtime": {
- "version": "7.24.5",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
- "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
- "dependencies": {
- "regenerator-runtime": "^0.14.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.24.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz",
- "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==",
- "dependencies": {
- "@babel/helper-string-parser": "^7.24.1",
- "@babel/helper-validator-identifier": "^7.24.5",
- "to-fast-properties": "^2.0.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@emotion/babel-plugin": {
- "version": "11.11.0",
- "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
- "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
- "dependencies": {
- "@babel/helper-module-imports": "^7.16.7",
- "@babel/runtime": "^7.18.3",
- "@emotion/hash": "^0.9.1",
- "@emotion/memoize": "^0.8.1",
- "@emotion/serialize": "^1.1.2",
- "babel-plugin-macros": "^3.1.0",
- "convert-source-map": "^1.5.0",
- "escape-string-regexp": "^4.0.0",
- "find-root": "^1.1.0",
- "source-map": "^0.5.7",
- "stylis": "4.2.0"
- }
- },
- "node_modules/@emotion/cache": {
- "version": "11.11.0",
- "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
- "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
- "dependencies": {
- "@emotion/memoize": "^0.8.1",
- "@emotion/sheet": "^1.2.2",
- "@emotion/utils": "^1.2.1",
- "@emotion/weak-memoize": "^0.3.1",
- "stylis": "4.2.0"
- }
- },
- "node_modules/@emotion/hash": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
- "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
- },
- "node_modules/@emotion/memoize": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
- "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
- },
- "node_modules/@emotion/react": {
- "version": "11.11.4",
- "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz",
- "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==",
- "dependencies": {
- "@babel/runtime": "^7.18.3",
- "@emotion/babel-plugin": "^11.11.0",
- "@emotion/cache": "^11.11.0",
- "@emotion/serialize": "^1.1.3",
- "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
- "@emotion/utils": "^1.2.1",
- "@emotion/weak-memoize": "^0.3.1",
- "hoist-non-react-statics": "^3.3.1"
- },
- "peerDependencies": {
- "react": ">=16.8.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@emotion/serialize": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz",
- "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==",
- "dependencies": {
- "@emotion/hash": "^0.9.1",
- "@emotion/memoize": "^0.8.1",
- "@emotion/unitless": "^0.8.1",
- "@emotion/utils": "^1.2.1",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/@emotion/sheet": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
- "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
- },
- "node_modules/@emotion/unitless": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
- "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
- },
- "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
- "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
- "peerDependencies": {
- "react": ">=16.8.0"
- }
- },
- "node_modules/@emotion/utils": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
- "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
- },
- "node_modules/@emotion/weak-memoize": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
- "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
- },
- "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,
- "peer": 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/regexpp": {
- "version": "4.10.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
- "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
- "dev": true,
- "peer": true,
- "engines": {
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
- }
- },
- "node_modules/@eslint/eslintrc": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
- "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "ajv": "^6.12.4",
- "debug": "^4.3.2",
- "espree": "^9.6.0",
- "globals": "^13.19.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": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/@eslint/eslintrc/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/@eslint/js": {
- "version": "8.57.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
- "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
- "dev": true,
- "peer": true,
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- }
- },
- "node_modules/@floating-ui/core": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.1.tgz",
- "integrity": "sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==",
- "dependencies": {
- "@floating-ui/utils": "^0.2.0"
- }
- },
- "node_modules/@floating-ui/dom": {
- "version": "1.6.5",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz",
- "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==",
- "dependencies": {
- "@floating-ui/core": "^1.0.0",
- "@floating-ui/utils": "^0.2.0"
- }
- },
- "node_modules/@floating-ui/utils": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz",
- "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw=="
- },
- "node_modules/@humanwhocodes/config-array": {
- "version": "0.11.14",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
- "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "@humanwhocodes/object-schema": "^2.0.2",
- "debug": "^4.3.1",
- "minimatch": "^3.0.5"
- },
- "engines": {
- "node": ">=10.10.0"
- }
- },
- "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "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,
- "peer": true,
- "engines": {
- "node": ">=12.22"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/object-schema": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
- "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
- "dev": true,
- "peer": true
- },
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "dev": true,
- "dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/@kurkle/color": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
- "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
- },
- "node_modules/@next/env": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz",
- "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA=="
- },
- "node_modules/@next/eslint-plugin-next": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.3.tgz",
- "integrity": "sha512-L3oDricIIjgj1AVnRdRor21gI7mShlSwU/1ZGHmqM3LzHhXXhdkrfeNY5zif25Bi5Dd7fiJHsbhoZCHfXYvlAw==",
- "dev": true,
- "dependencies": {
- "glob": "10.3.10"
- }
- },
- "node_modules/@next/swc-darwin-arm64": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz",
- "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-darwin-x64": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz",
- "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-linux-arm64-gnu": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz",
- "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-linux-arm64-musl": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz",
- "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-linux-x64-gnu": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz",
- "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-linux-x64-musl": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz",
- "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-win32-arm64-msvc": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz",
- "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==",
- "cpu": [
- "arm64"
- ],
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-win32-ia32-msvc": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz",
- "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==",
- "cpu": [
- "ia32"
- ],
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-win32-x64-msvc": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz",
- "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==",
- "cpu": [
- "x64"
- ],
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "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/@panva/hkdf": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz",
- "integrity": "sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==",
- "funding": {
- "url": "https://github.com/sponsors/panva"
- }
- },
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
- "optional": true,
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@rushstack/eslint-patch": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.2.tgz",
- "integrity": "sha512-hw437iINopmQuxWPSUEvqE56NCPsiU8N4AYtfHmJFckclktzK9YQJieD3XkDCDH4OjL+C7zgPUh73R/nrcHrqw==",
- "dev": true
- },
- "node_modules/@swc/counter": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
- "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
- },
- "node_modules/@swc/helpers": {
- "version": "0.5.5",
- "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
- "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
- "dependencies": {
- "@swc/counter": "^0.1.3",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tabler/icons": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.3.0.tgz",
- "integrity": "sha512-PLVe9d7b59sKytbx00KgeGhQG3N176Ezv8YMmsnSz4s0ifDzMWlp/h2wEfQZ0ZNe8e377GY2OW6kovUe3Rnd0g==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/codecalm"
- }
- },
- "node_modules/@tabler/icons-react": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.3.0.tgz",
- "integrity": "sha512-Qn1Po+0gErh1zCWlaOdoVoGqeonWfSuiboYgwZBs6PIJNsj7yr3bIa4BkHmgJgtlXLT9LvCzt/RvwlgjxLfjjg==",
- "dependencies": {
- "@tabler/icons": "3.3.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/codecalm"
- },
- "peerDependencies": {
- "react": ">= 16"
- }
- },
- "node_modules/@types/json5": {
- "version": "0.0.29",
- "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
- "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
- "dev": true
- },
- "node_modules/@types/parse-json": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
- "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
- },
- "node_modules/@types/prop-types": {
- "version": "15.7.12",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
- "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
- },
- "node_modules/@types/react": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz",
- "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==",
- "dependencies": {
- "@types/prop-types": "*",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "18.3.0",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
- "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
- "peer": true,
- "dependencies": {
- "@types/react": "*"
- }
- },
- "node_modules/@types/react-transition-group": {
- "version": "4.4.10",
- "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
- "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
- "dependencies": {
- "@types/react": "*"
- }
- },
- "node_modules/@typescript-eslint/parser": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz",
- "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/scope-manager": "7.2.0",
- "@typescript-eslint/types": "7.2.0",
- "@typescript-eslint/typescript-estree": "7.2.0",
- "@typescript-eslint/visitor-keys": "7.2.0",
- "debug": "^4.3.4"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.56.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz",
- "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "7.2.0",
- "@typescript-eslint/visitor-keys": "7.2.0"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/types": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz",
- "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==",
- "dev": true,
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz",
- "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "7.2.0",
- "@typescript-eslint/visitor-keys": "7.2.0",
- "debug": "^4.3.4",
- "globby": "^11.1.0",
- "is-glob": "^4.0.3",
- "minimatch": "9.0.3",
- "semver": "^7.5.4",
- "ts-api-utils": "^1.0.1"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz",
- "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "7.2.0",
- "eslint-visitor-keys": "^3.4.1"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@ungap/structured-clone": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
- "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
- "dev": true,
- "peer": true
- },
- "node_modules/acorn": {
- "version": "8.11.3",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
- "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
- "dev": true,
- "peer": true,
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "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,
- "peer": true,
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
- "peer": true
- },
- "node_modules/aria-query": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
- "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
- "dev": true,
- "dependencies": {
- "dequal": "^2.0.3"
- }
- },
- "node_modules/array-buffer-byte-length": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
- "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.5",
- "is-array-buffer": "^3.0.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array-includes": {
- "version": "3.1.8",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
- "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-object-atoms": "^1.0.0",
- "get-intrinsic": "^1.2.4",
- "is-string": "^1.0.7"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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/array.prototype.findlast": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
- "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.findlastindex": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz",
- "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.flat": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
- "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.flatmap": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
- "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/array.prototype.toreversed": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz",
- "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "es-shim-unscopables": "^1.0.0"
- }
- },
- "node_modules/array.prototype.tosorted": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz",
- "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.5",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.22.3",
- "es-errors": "^1.1.0",
- "es-shim-unscopables": "^1.0.2"
- }
- },
- "node_modules/arraybuffer.prototype.slice": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
- "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
- "dev": true,
- "dependencies": {
- "array-buffer-byte-length": "^1.0.1",
- "call-bind": "^1.0.5",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.22.3",
- "es-errors": "^1.2.1",
- "get-intrinsic": "^1.2.3",
- "is-array-buffer": "^3.0.4",
- "is-shared-array-buffer": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/ast-types-flow": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
- "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
- "dev": true
- },
- "node_modules/available-typed-arrays": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
- "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
- "dev": true,
- "dependencies": {
- "possible-typed-array-names": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/axe-core": {
- "version": "4.7.0",
- "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz",
- "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/axobject-query": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
- "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==",
- "dev": true,
- "dependencies": {
- "dequal": "^2.0.3"
- }
- },
- "node_modules/babel-plugin-macros": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
- "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
- "dependencies": {
- "@babel/runtime": "^7.12.5",
- "cosmiconfig": "^7.0.0",
- "resolve": "^1.19.0"
- },
- "engines": {
- "node": ">=10",
- "npm": ">=6"
- }
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
- },
- "node_modules/bcryptjs": {
- "version": "2.4.3",
- "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
- "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
- },
- "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/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
- "dev": true,
- "dependencies": {
- "fill-range": "^7.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/busboy": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
- "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
- "dependencies": {
- "streamsearch": "^1.1.0"
- },
- "engines": {
- "node": ">=10.16.0"
- }
- },
- "node_modules/call-bind": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
- "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
- "dev": true,
- "dependencies": {
- "es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.4",
- "set-function-length": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001617",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz",
- "integrity": "sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ]
- },
- "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,
- "peer": 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/chart.js": {
- "version": "4.4.2",
- "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
- "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
- "dependencies": {
- "@kurkle/color": "^0.3.0"
- },
- "engines": {
- "pnpm": ">=8"
- }
- },
- "node_modules/client-only": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
- "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
- },
- "node_modules/clsx": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true
- },
- "node_modules/convert-source-map": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
- "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
- },
- "node_modules/cookie": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
- "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cosmiconfig": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
- "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
- "dependencies": {
- "@types/parse-json": "^4.0.0",
- "import-fresh": "^3.2.1",
- "parse-json": "^5.0.0",
- "path-type": "^4.0.0",
- "yaml": "^1.10.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "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/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
- },
- "node_modules/damerau-levenshtein": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
- "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
- "dev": true
- },
- "node_modules/data-view-buffer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
- "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.6",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/data-view-byte-length": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz",
- "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/data-view-byte-offset": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz",
- "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.6",
- "es-errors": "^1.3.0",
- "is-data-view": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
- "dependencies": {
- "ms": "2.1.2"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "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,
- "peer": 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",
- "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
- "dev": true,
- "dependencies": {
- "es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "gopd": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/define-properties": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
- "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
- "dev": true,
- "dependencies": {
- "define-data-property": "^1.0.1",
- "has-property-descriptors": "^1.0.0",
- "object-keys": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/dequal": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
- "dev": true,
- "engines": {
- "node": ">=6"
- }
- },
- "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",
- "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/dom-helpers": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
- "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
- "dependencies": {
- "@babel/runtime": "^7.8.7",
- "csstype": "^3.0.2"
- }
- },
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true
- },
- "node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true
- },
- "node_modules/enhanced-resolve": {
- "version": "5.16.1",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz",
- "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==",
- "dev": true,
- "dependencies": {
- "graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "dependencies": {
- "is-arrayish": "^0.2.1"
- }
- },
- "node_modules/es-abstract": {
- "version": "1.23.3",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
- "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==",
- "dev": true,
- "dependencies": {
- "array-buffer-byte-length": "^1.0.1",
- "arraybuffer.prototype.slice": "^1.0.3",
- "available-typed-arrays": "^1.0.7",
- "call-bind": "^1.0.7",
- "data-view-buffer": "^1.0.1",
- "data-view-byte-length": "^1.0.1",
- "data-view-byte-offset": "^1.0.0",
- "es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "es-set-tostringtag": "^2.0.3",
- "es-to-primitive": "^1.2.1",
- "function.prototype.name": "^1.1.6",
- "get-intrinsic": "^1.2.4",
- "get-symbol-description": "^1.0.2",
- "globalthis": "^1.0.3",
- "gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.2",
- "has-proto": "^1.0.3",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.2",
- "internal-slot": "^1.0.7",
- "is-array-buffer": "^3.0.4",
- "is-callable": "^1.2.7",
- "is-data-view": "^1.0.1",
- "is-negative-zero": "^2.0.3",
- "is-regex": "^1.1.4",
- "is-shared-array-buffer": "^1.0.3",
- "is-string": "^1.0.7",
- "is-typed-array": "^1.1.13",
- "is-weakref": "^1.0.2",
- "object-inspect": "^1.13.1",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.5",
- "regexp.prototype.flags": "^1.5.2",
- "safe-array-concat": "^1.1.2",
- "safe-regex-test": "^1.0.3",
- "string.prototype.trim": "^1.2.9",
- "string.prototype.trimend": "^1.0.8",
- "string.prototype.trimstart": "^1.0.8",
- "typed-array-buffer": "^1.0.2",
- "typed-array-byte-length": "^1.0.1",
- "typed-array-byte-offset": "^1.0.2",
- "typed-array-length": "^1.0.6",
- "unbox-primitive": "^1.0.2",
- "which-typed-array": "^1.1.15"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/es-define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
- "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
- "dev": true,
- "dependencies": {
- "get-intrinsic": "^1.2.4"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-iterator-helpers": {
- "version": "1.0.19",
- "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz",
- "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.3",
- "es-errors": "^1.3.0",
- "es-set-tostringtag": "^2.0.3",
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.4",
- "globalthis": "^1.0.3",
- "has-property-descriptors": "^1.0.2",
- "has-proto": "^1.0.3",
- "has-symbols": "^1.0.3",
- "internal-slot": "^1.0.7",
- "iterator.prototype": "^1.1.2",
- "safe-array-concat": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-object-atoms": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
- "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
- "dev": true,
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-set-tostringtag": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
- "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
- "dev": true,
- "dependencies": {
- "get-intrinsic": "^1.2.4",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-shim-unscopables": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
- "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
- "dev": true,
- "dependencies": {
- "hasown": "^2.0.0"
- }
- },
- "node_modules/es-to-primitive": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
- "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
- "dev": true,
- "dependencies": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint": {
- "version": "8.57.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
- "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.6.1",
- "@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.57.0",
- "@humanwhocodes/config-array": "^0.11.14",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@nodelib/fs.walk": "^1.2.8",
- "@ungap/structured-clone": "^1.2.0",
- "ajv": "^6.12.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.2",
- "debug": "^4.3.2",
- "doctrine": "^3.0.0",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^7.2.2",
- "eslint-visitor-keys": "^3.4.3",
- "espree": "^9.6.1",
- "esquery": "^1.4.2",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^6.0.1",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "globals": "^13.19.0",
- "graphemer": "^1.4.0",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "is-path-inside": "^3.0.3",
- "js-yaml": "^4.1.0",
- "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": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint-config-next": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.3.tgz",
- "integrity": "sha512-ZkNztm3Q7hjqvB1rRlOX8P9E/cXRL9ajRcs8jufEtwMfTVYRqnmtnaSu57QqHyBlovMuiB8LEzfLBkh5RYV6Fg==",
- "dev": true,
- "dependencies": {
- "@next/eslint-plugin-next": "14.2.3",
- "@rushstack/eslint-patch": "^1.3.3",
- "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0",
- "eslint-import-resolver-node": "^0.3.6",
- "eslint-import-resolver-typescript": "^3.5.2",
- "eslint-plugin-import": "^2.28.1",
- "eslint-plugin-jsx-a11y": "^6.7.1",
- "eslint-plugin-react": "^7.33.2",
- "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
- },
- "peerDependencies": {
- "eslint": "^7.23.0 || ^8.0.0",
- "typescript": ">=3.3.1"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-import-resolver-node": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
- "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
- "dev": true,
- "dependencies": {
- "debug": "^3.2.7",
- "is-core-module": "^2.13.0",
- "resolve": "^1.22.4"
- }
- },
- "node_modules/eslint-import-resolver-node/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/eslint-import-resolver-typescript": {
- "version": "3.6.1",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz",
- "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==",
- "dev": true,
- "dependencies": {
- "debug": "^4.3.4",
- "enhanced-resolve": "^5.12.0",
- "eslint-module-utils": "^2.7.4",
- "fast-glob": "^3.3.1",
- "get-tsconfig": "^4.5.0",
- "is-core-module": "^2.11.0",
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": "^14.18.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts"
- },
- "peerDependencies": {
- "eslint": "*",
- "eslint-plugin-import": "*"
- }
- },
- "node_modules/eslint-module-utils": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
- "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==",
- "dev": true,
- "dependencies": {
- "debug": "^3.2.7"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependenciesMeta": {
- "eslint": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-module-utils/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/eslint-plugin-import": {
- "version": "2.29.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
- "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
- "dev": true,
- "dependencies": {
- "array-includes": "^3.1.7",
- "array.prototype.findlastindex": "^1.2.3",
- "array.prototype.flat": "^1.3.2",
- "array.prototype.flatmap": "^1.3.2",
- "debug": "^3.2.7",
- "doctrine": "^2.1.0",
- "eslint-import-resolver-node": "^0.3.9",
- "eslint-module-utils": "^2.8.0",
- "hasown": "^2.0.0",
- "is-core-module": "^2.13.1",
- "is-glob": "^4.0.3",
- "minimatch": "^3.1.2",
- "object.fromentries": "^2.0.7",
- "object.groupby": "^1.0.1",
- "object.values": "^1.1.7",
- "semver": "^6.3.1",
- "tsconfig-paths": "^3.15.0"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
- }
- },
- "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/eslint-plugin-import/node_modules/debug": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
- "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
- "dev": true,
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/eslint-plugin-import/node_modules/doctrine": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
- "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
- "dev": true,
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/eslint-plugin-import/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/eslint-plugin-import/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/eslint-plugin-jsx-a11y": {
- "version": "6.8.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz",
- "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==",
- "dev": true,
- "dependencies": {
- "@babel/runtime": "^7.23.2",
- "aria-query": "^5.3.0",
- "array-includes": "^3.1.7",
- "array.prototype.flatmap": "^1.3.2",
- "ast-types-flow": "^0.0.8",
- "axe-core": "=4.7.0",
- "axobject-query": "^3.2.1",
- "damerau-levenshtein": "^1.0.8",
- "emoji-regex": "^9.2.2",
- "es-iterator-helpers": "^1.0.15",
- "hasown": "^2.0.0",
- "jsx-ast-utils": "^3.3.5",
- "language-tags": "^1.0.9",
- "minimatch": "^3.1.2",
- "object.entries": "^1.1.7",
- "object.fromentries": "^2.0.7"
- },
- "engines": {
- "node": ">=4.0"
- },
- "peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
- }
- },
- "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/eslint-plugin-react": {
- "version": "7.34.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz",
- "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==",
- "dev": true,
- "dependencies": {
- "array-includes": "^3.1.7",
- "array.prototype.findlast": "^1.2.4",
- "array.prototype.flatmap": "^1.3.2",
- "array.prototype.toreversed": "^1.1.2",
- "array.prototype.tosorted": "^1.1.3",
- "doctrine": "^2.1.0",
- "es-iterator-helpers": "^1.0.17",
- "estraverse": "^5.3.0",
- "jsx-ast-utils": "^2.4.1 || ^3.0.0",
- "minimatch": "^3.1.2",
- "object.entries": "^1.1.7",
- "object.fromentries": "^2.0.7",
- "object.hasown": "^1.1.3",
- "object.values": "^1.1.7",
- "prop-types": "^15.8.1",
- "resolve": "^2.0.0-next.5",
- "semver": "^6.3.1",
- "string.prototype.matchall": "^4.0.10"
- },
- "engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
- }
- },
- "node_modules/eslint-plugin-react-hooks": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
- "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
- }
- },
- "node_modules/eslint-plugin-react/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/eslint-plugin-react/node_modules/doctrine": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
- "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
- "dev": true,
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/eslint-plugin-react/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/eslint-plugin-react/node_modules/resolve": {
- "version": "2.0.0-next.5",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
- "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
- "dev": true,
- "dependencies": {
- "is-core-module": "^2.13.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/eslint-plugin-react/node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/eslint-scope": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
- "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "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/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/eslint/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/espree": {
- "version": "9.6.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
- "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "acorn": "^8.9.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^3.4.1"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/esquery": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
- "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
- "dev": true,
- "peer": 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,
- "peer": 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",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
- "peer": true
- },
- "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-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "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==",
- "dev": true,
- "peer": true
- },
- "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,
- "peer": true
- },
- "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": "6.0.1",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
- "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "flat-cache": "^3.0.4"
- },
- "engines": {
- "node": "^10.12.0 || >=12.0.0"
- }
- },
- "node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
- "dev": true,
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/find-root": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
- "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
- },
- "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,
- "peer": 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": "3.2.0",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
- "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.3",
- "rimraf": "^3.0.2"
- },
- "engines": {
- "node": "^10.12.0 || >=12.0.0"
- }
- },
- "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,
- "peer": true
- },
- "node_modules/for-each": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
- "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
- "dev": true,
- "dependencies": {
- "is-callable": "^1.1.3"
- }
- },
- "node_modules/foreground-child": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
- "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
- "dev": true,
- "dependencies": {
- "cross-spawn": "^7.0.0",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true,
- "peer": true
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/function.prototype.name": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
- "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1",
- "functions-have-names": "^1.2.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/functions-have-names": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
- "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
- "dev": true,
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
- "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
- "dev": true,
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-symbol-description": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
- "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.5",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-tsconfig": {
- "version": "4.7.5",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz",
- "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==",
- "dev": true,
- "dependencies": {
- "resolve-pkg-maps": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
- }
- },
- "node_modules/glob": {
- "version": "10.3.10",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
- "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
- "dev": true,
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^2.3.5",
- "minimatch": "^9.0.1",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
- "path-scurry": "^1.10.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/glob/node_modules/minimatch": {
- "version": "9.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
- "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/globals": {
- "version": "13.24.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
- "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "type-fest": "^0.20.2"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/globalthis": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
- "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
- "dev": true,
- "dependencies": {
- "define-properties": "^1.2.1",
- "gopd": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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",
- "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
- "dev": true,
- "dependencies": {
- "get-intrinsic": "^1.1.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
- },
- "node_modules/graphemer": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
- "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true,
- "peer": true
- },
- "node_modules/has-bigints": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
- "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
- "dev": true,
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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,
- "peer": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/has-property-descriptors": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
- "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
- "dev": true,
- "dependencies": {
- "es-define-property": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-proto": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
- "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-tostringtag": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
- "dependencies": {
- "has-symbols": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "dependencies": {
- "react-is": "^16.7.0"
- }
- },
- "node_modules/ignore": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
- "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
- "dev": true,
- "engines": {
- "node": ">= 4"
- }
- },
- "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==",
- "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,
- "peer": true,
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "node_modules/inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true,
- "peer": true
- },
- "node_modules/internal-slot": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
- "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
- "dev": true,
- "dependencies": {
- "es-errors": "^1.3.0",
- "hasown": "^2.0.0",
- "side-channel": "^1.0.4"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/is-array-buffer": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
- "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-arrayish": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
- },
- "node_modules/is-async-function": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
- "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
- "dev": true,
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-bigint": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
- "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
- "dev": true,
- "dependencies": {
- "has-bigints": "^1.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-boolean-object": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
- "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-callable": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
- "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-core-module": {
- "version": "2.13.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
- "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
- "dependencies": {
- "hasown": "^2.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-data-view": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz",
- "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==",
- "dev": true,
- "dependencies": {
- "is-typed-array": "^1.1.13"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-date-object": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
- "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
- "dev": true,
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-finalizationregistry": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
- "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-generator-function": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
- "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
- "dev": true,
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-map": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
- "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-negative-zero": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
- "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/is-number-object": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
- "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
- "dev": true,
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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,
- "peer": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-regex": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
- "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-set": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
- "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-shared-array-buffer": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
- "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-string": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
- "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
- "dev": true,
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-symbol": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
- "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
- "dev": true,
- "dependencies": {
- "has-symbols": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-typed-array": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
- "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
- "dev": true,
- "dependencies": {
- "which-typed-array": "^1.1.14"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-weakmap": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
- "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-weakref": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
- "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-weakset": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz",
- "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "get-intrinsic": "^1.2.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/isarray": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
- "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
- "dev": true
- },
- "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/iterator.prototype": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
- "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==",
- "dev": true,
- "dependencies": {
- "define-properties": "^1.2.1",
- "get-intrinsic": "^1.2.1",
- "has-symbols": "^1.0.3",
- "reflect.getprototypeof": "^1.0.4",
- "set-function-name": "^2.0.1"
- }
- },
- "node_modules/jackspeak": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
- "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
- "dev": true,
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
- "node_modules/jose": {
- "version": "4.15.5",
- "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz",
- "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==",
- "funding": {
- "url": "https://github.com/sponsors/panva"
- }
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
- },
- "node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "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,
- "peer": true
- },
- "node_modules/json-parse-even-better-errors": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
- "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
- },
- "node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "peer": true
- },
- "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,
- "peer": true
- },
- "node_modules/json5": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
- "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
- "dev": true,
- "dependencies": {
- "minimist": "^1.2.0"
- },
- "bin": {
- "json5": "lib/cli.js"
- }
- },
- "node_modules/jsx-ast-utils": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
- "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
- "dev": true,
- "dependencies": {
- "array-includes": "^3.1.6",
- "array.prototype.flat": "^1.3.1",
- "object.assign": "^4.1.4",
- "object.values": "^1.1.6"
- },
- "engines": {
- "node": ">=4.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,
- "peer": true,
- "dependencies": {
- "json-buffer": "3.0.1"
- }
- },
- "node_modules/language-subtag-registry": {
- "version": "0.3.22",
- "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
- "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
- "dev": true
- },
- "node_modules/language-tags": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
- "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
- "dev": true,
- "dependencies": {
- "language-subtag-registry": "^0.3.20"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "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,
- "peer": true,
- "dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
- },
- "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,
- "peer": true,
- "dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "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,
- "peer": true
- },
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
- "node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/memoize-one": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
- "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
- },
- "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/micromatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
- "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
- "dev": true,
- "dependencies": {
- "braces": "^3.0.2",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/minimatch": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
- "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "dev": true,
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/minipass": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz",
- "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==",
- "dev": true,
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "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/nanoid": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "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,
- "peer": true
- },
- "node_modules/next": {
- "version": "14.2.3",
- "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz",
- "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==",
- "dependencies": {
- "@next/env": "14.2.3",
- "@swc/helpers": "0.5.5",
- "busboy": "1.6.0",
- "caniuse-lite": "^1.0.30001579",
- "graceful-fs": "^4.2.11",
- "postcss": "8.4.31",
- "styled-jsx": "5.1.1"
- },
- "bin": {
- "next": "dist/bin/next"
- },
- "engines": {
- "node": ">=18.17.0"
- },
- "optionalDependencies": {
- "@next/swc-darwin-arm64": "14.2.3",
- "@next/swc-darwin-x64": "14.2.3",
- "@next/swc-linux-arm64-gnu": "14.2.3",
- "@next/swc-linux-arm64-musl": "14.2.3",
- "@next/swc-linux-x64-gnu": "14.2.3",
- "@next/swc-linux-x64-musl": "14.2.3",
- "@next/swc-win32-arm64-msvc": "14.2.3",
- "@next/swc-win32-ia32-msvc": "14.2.3",
- "@next/swc-win32-x64-msvc": "14.2.3"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.1.0",
- "@playwright/test": "^1.41.2",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "sass": "^1.3.0"
- },
- "peerDependenciesMeta": {
- "@opentelemetry/api": {
- "optional": true
- },
- "@playwright/test": {
- "optional": true
- },
- "sass": {
- "optional": true
- }
- }
- },
- "node_modules/next-auth": {
- "version": "4.24.7",
- "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.7.tgz",
- "integrity": "sha512-iChjE8ov/1K/z98gdKbn2Jw+2vLgJtVV39X+rCP5SGnVQuco7QOr19FRNGMIrD8d3LYhHWV9j9sKLzq1aDWWQQ==",
- "dependencies": {
- "@babel/runtime": "^7.20.13",
- "@panva/hkdf": "^1.0.2",
- "cookie": "^0.5.0",
- "jose": "^4.15.5",
- "oauth": "^0.9.15",
- "openid-client": "^5.4.0",
- "preact": "^10.6.3",
- "preact-render-to-string": "^5.1.19",
- "uuid": "^8.3.2"
- },
- "peerDependencies": {
- "next": "^12.2.5 || ^13 || ^14",
- "nodemailer": "^6.6.5",
- "react": "^17.0.2 || ^18",
- "react-dom": "^17.0.2 || ^18"
- },
- "peerDependenciesMeta": {
- "nodemailer": {
- "optional": true
- }
- }
- },
- "node_modules/nodemailer": {
- "version": "6.9.13",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz",
- "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/oauth": {
- "version": "0.9.15",
- "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
- "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-hash": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
- "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/object-inspect": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
- "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
- "dev": true,
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object-keys": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.assign": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
- "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.5",
- "define-properties": "^1.2.1",
- "has-symbols": "^1.0.3",
- "object-keys": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object.entries": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
- "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.fromentries": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
- "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object.groupby": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
- "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.hasown": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz",
- "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==",
- "dev": true,
- "dependencies": {
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object.values": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz",
- "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/oidc-token-hash": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
- "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
- "engines": {
- "node": "^10.13.0 || >=12.0.0"
- }
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/openid-client": {
- "version": "5.6.5",
- "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz",
- "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==",
- "dependencies": {
- "jose": "^4.15.5",
- "lru-cache": "^6.0.0",
- "object-hash": "^2.2.0",
- "oidc-token-hash": "^5.0.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/panva"
- }
- },
- "node_modules/optionator": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
- "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
- "dev": true,
- "peer": 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,
- "peer": 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,
- "peer": 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==",
- "dependencies": {
- "callsites": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/parse-json": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
- "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
- "dependencies": {
- "@babel/code-frame": "^7.0.0",
- "error-ex": "^1.3.1",
- "json-parse-even-better-errors": "^2.3.0",
- "lines-and-columns": "^1.1.6"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/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,
- "peer": 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",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "dev": true,
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
- },
- "node_modules/path-scurry": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.0.tgz",
- "integrity": "sha512-LNHTaVkzaYaLGlO+0u3rQTz7QrHTFOuKyba9JMTQutkmtNew8dw8wOD7mTU/5fCPZzCWpfW0XnQKzY61P0aTaw==",
- "dev": true,
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/path-scurry/node_modules/lru-cache": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
- "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
- "dev": true,
- "engines": {
- "node": "14 || >=16.14"
- }
- },
- "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==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/possible-typed-array-names": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
- "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
- "dev": true,
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/postcss": {
- "version": "8.4.31",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
- "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "dependencies": {
- "nanoid": "^3.3.6",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/preact": {
- "version": "10.21.0",
- "resolved": "https://registry.npmjs.org/preact/-/preact-10.21.0.tgz",
- "integrity": "sha512-aQAIxtzWEwH8ou+OovWVSVNlFImL7xUCwJX3YMqA3U8iKCNC34999fFOnWjYNsylgfPgMexpbk7WYOLtKr/mxg==",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/preact"
- }
- },
- "node_modules/preact-render-to-string": {
- "version": "5.2.6",
- "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
- "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
- "dependencies": {
- "pretty-format": "^3.8.0"
- },
- "peerDependencies": {
- "preact": ">=10"
- }
- },
- "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,
- "peer": true,
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/prettier": {
- "version": "3.2.5",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
- "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
- "dev": true,
- "bin": {
- "prettier": "bin/prettier.cjs"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/prettier/prettier?sponsor=1"
- }
- },
- "node_modules/pretty-format": {
- "version": "3.8.0",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
- "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
- },
- "node_modules/prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
- }
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
- "peer": true,
- "engines": {
- "node": ">=6"
- }
- },
- "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/react": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
- "dependencies": {
- "loose-envify": "^1.1.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-chartjs-2": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz",
- "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==",
- "peerDependencies": {
- "chart.js": "^4.1.1",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/react-dom": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
- "dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
- },
- "peerDependencies": {
- "react": "^18.3.1"
- }
- },
- "node_modules/react-hook-form": {
- "version": "7.51.4",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz",
- "integrity": "sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==",
- "engines": {
- "node": ">=12.22.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/react-hook-form"
- },
- "peerDependencies": {
- "react": "^16.8.0 || ^17 || ^18"
- }
- },
- "node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
- },
- "node_modules/react-select": {
- "version": "5.8.0",
- "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz",
- "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==",
- "dependencies": {
- "@babel/runtime": "^7.12.0",
- "@emotion/cache": "^11.4.0",
- "@emotion/react": "^11.8.1",
- "@floating-ui/dom": "^1.0.1",
- "@types/react-transition-group": "^4.4.0",
- "memoize-one": "^6.0.0",
- "prop-types": "^15.6.0",
- "react-transition-group": "^4.3.0",
- "use-isomorphic-layout-effect": "^1.1.2"
- },
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
- "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/react-toastify": {
- "version": "10.0.5",
- "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz",
- "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==",
- "dependencies": {
- "clsx": "^2.1.0"
- },
- "peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
- }
- },
- "node_modules/react-transition-group": {
- "version": "4.4.5",
- "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
- "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
- "dependencies": {
- "@babel/runtime": "^7.5.5",
- "dom-helpers": "^5.0.1",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.6.2"
- },
- "peerDependencies": {
- "react": ">=16.6.0",
- "react-dom": ">=16.6.0"
- }
- },
- "node_modules/reflect.getprototypeof": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
- "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.1",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.4",
- "globalthis": "^1.0.3",
- "which-builtin-type": "^1.1.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/regenerator-runtime": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
- },
- "node_modules/regexp.prototype.flags": {
- "version": "1.5.2",
- "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
- "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.6",
- "define-properties": "^1.2.1",
- "es-errors": "^1.3.0",
- "set-function-name": "^2.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/resolve": {
- "version": "1.22.8",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
- "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
- "dependencies": {
- "is-core-module": "^2.13.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/resolve-pkg-maps": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
- "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
- "dev": true,
- "funding": {
- "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
- }
- },
- "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/rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/rimraf/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/rimraf/node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/rimraf/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "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-array-concat": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
- "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "get-intrinsic": "^1.2.4",
- "has-symbols": "^1.0.3",
- "isarray": "^2.0.5"
- },
- "engines": {
- "node": ">=0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/safe-regex-test": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
- "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.6",
- "es-errors": "^1.3.0",
- "is-regex": "^1.1.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/scheduler": {
- "version": "0.23.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
- "dependencies": {
- "loose-envify": "^1.1.0"
- }
- },
- "node_modules/semver": {
- "version": "7.6.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
- "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
- "dev": true,
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/set-function-length": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
- "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
- "dev": true,
- "dependencies": {
- "define-data-property": "^1.1.4",
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.4",
- "gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/set-function-name": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
- "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
- "dev": true,
- "dependencies": {
- "define-data-property": "^1.1.4",
- "es-errors": "^1.3.0",
- "functions-have-names": "^1.2.3",
- "has-property-descriptors": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "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",
- "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.4",
- "object-inspect": "^1.13.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "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/source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
- "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/spinners-react": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/spinners-react/-/spinners-react-1.0.7.tgz",
- "integrity": "sha512-Xcgpc7Ybm6HOrpCVJjbH1G/NV852HaV4Zc9T1sJ2+S2hn05lGiBZS1dBOKGLc1kp8wv2sd3wtt94I/NNqDjs3Q==",
- "peerDependencies": {
- "@types/react": "^16.x || ^17.x || ^18.x",
- "@types/react-dom": "^16.x || ^17.x || ^18.x",
- "react": "^16.x || ^17.x || ^18.x",
- "react-dom": "^16.x || ^17.x || ^18.x"
- }
- },
- "node_modules/streamsearch": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
- "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/string-width-cjs": {
- "name": "string-width",
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
- },
- "node_modules/string-width/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/string-width/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/string.prototype.matchall": {
- "version": "4.0.11",
- "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz",
- "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "get-intrinsic": "^1.2.4",
- "gopd": "^1.0.1",
- "has-symbols": "^1.0.3",
- "internal-slot": "^1.0.7",
- "regexp.prototype.flags": "^1.5.2",
- "set-function-name": "^2.0.2",
- "side-channel": "^1.0.6"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/string.prototype.trim": {
- "version": "1.2.9",
- "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
- "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.0",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/string.prototype.trimend": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz",
- "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/string.prototype.trimstart": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
- "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-bom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
- "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "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,
- "peer": true,
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/styled-jsx": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
- "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
- "dependencies": {
- "client-only": "0.0.1"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "peerDependencies": {
- "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
- },
- "peerDependenciesMeta": {
- "@babel/core": {
- "optional": true
- },
- "babel-plugin-macros": {
- "optional": true
- }
- }
- },
- "node_modules/stylis": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
- "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
- },
- "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,
- "peer": true,
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/swr": {
- "version": "2.2.5",
- "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz",
- "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
- "dependencies": {
- "client-only": "^0.0.1",
- "use-sync-external-store": "^1.2.0"
- },
- "peerDependencies": {
- "react": "^16.11.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/tapable": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
- "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
- "dev": true,
- "engines": {
- "node": ">=6"
- }
- },
- "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,
- "peer": true
- },
- "node_modules/to-fast-properties": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
- "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "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/tsconfig-paths": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
- "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
- "dev": true,
- "dependencies": {
- "@types/json5": "^0.0.29",
- "json5": "^1.0.2",
- "minimist": "^1.2.6",
- "strip-bom": "^3.0.0"
- }
- },
- "node_modules/tslib": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
- "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
- },
- "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,
- "peer": true,
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/type-fest": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
- "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true,
- "peer": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/typed-array-buffer": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
- "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "es-errors": "^1.3.0",
- "is-typed-array": "^1.1.13"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/typed-array-byte-length": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
- "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "for-each": "^0.3.3",
- "gopd": "^1.0.1",
- "has-proto": "^1.0.3",
- "is-typed-array": "^1.1.13"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/typed-array-byte-offset": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
- "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
- "dev": true,
- "dependencies": {
- "available-typed-arrays": "^1.0.7",
- "call-bind": "^1.0.7",
- "for-each": "^0.3.3",
- "gopd": "^1.0.1",
- "has-proto": "^1.0.3",
- "is-typed-array": "^1.1.13"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/typed-array-length": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz",
- "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.7",
- "for-each": "^0.3.3",
- "gopd": "^1.0.1",
- "has-proto": "^1.0.3",
- "is-typed-array": "^1.1.13",
- "possible-typed-array-names": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/typescript": {
- "version": "5.4.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
- "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
- "dev": true,
- "peer": true,
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/unbox-primitive": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
- "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
- "dev": true,
- "dependencies": {
- "call-bind": "^1.0.2",
- "has-bigints": "^1.0.2",
- "has-symbols": "^1.0.3",
- "which-boxed-primitive": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
- "node_modules/use-isomorphic-layout-effect": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
- "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/use-sync-external-store": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
- "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
- }
- },
- "node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
- "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-boxed-primitive": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
- "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
- "dev": true,
- "dependencies": {
- "is-bigint": "^1.0.1",
- "is-boolean-object": "^1.1.0",
- "is-number-object": "^1.0.4",
- "is-string": "^1.0.5",
- "is-symbol": "^1.0.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/which-builtin-type": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
- "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
- "dev": true,
- "dependencies": {
- "function.prototype.name": "^1.1.5",
- "has-tostringtag": "^1.0.0",
- "is-async-function": "^2.0.0",
- "is-date-object": "^1.0.5",
- "is-finalizationregistry": "^1.0.2",
- "is-generator-function": "^1.0.10",
- "is-regex": "^1.1.4",
- "is-weakref": "^1.0.2",
- "isarray": "^2.0.5",
- "which-boxed-primitive": "^1.0.2",
- "which-collection": "^1.0.1",
- "which-typed-array": "^1.1.9"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/which-collection": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
- "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
- "dev": true,
- "dependencies": {
- "is-map": "^2.0.3",
- "is-set": "^2.0.3",
- "is-weakmap": "^2.0.2",
- "is-weakset": "^2.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/which-typed-array": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
- "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
- "dev": true,
- "dependencies": {
- "available-typed-arrays": "^1.0.7",
- "call-bind": "^1.0.7",
- "for-each": "^0.3.3",
- "gopd": "^1.0.1",
- "has-tostringtag": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "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,
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
- },
- "node_modules/wrap-ansi-cjs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/wrap-ansi/node_modules/ansi-styles": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
- "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/wrap-ansi/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true,
- "peer": true
- },
- "node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- },
- "node_modules/yaml": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
- "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
- "engines": {
- "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,
- "peer": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- }
- }
-}
diff --git a/package.json b/package.json
index f94b117..affd3a8 100644
--- a/package.json
+++ b/package.json
@@ -1,31 +1,52 @@
{
- "name": "borgwarehouse",
- "version": "2.3.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "next lint"
- },
- "dependencies": {
- "@tabler/icons-react": "^3.3.0",
- "bcryptjs": "^2.4.3",
- "chart.js": "^4.4.2",
- "next": "^14.2.3",
- "next-auth": "^4.24.7",
- "nodemailer": "^6.9.13",
- "react": "^18.3.1",
- "react-chartjs-2": "^5.2.0",
- "react-dom": "^18.3.1",
- "react-hook-form": "^7.51.4",
- "react-select": "^5.8.0",
- "react-toastify": "^10.0.5",
- "spinners-react": "^1.0.7",
- "swr": "^2.2.5"
- },
- "devDependencies": {
- "eslint-config-next": "^14.2.3",
- "prettier": "^3.2.5"
- }
+ "name": "borgwarehouse",
+ "version": "3.1.2",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "pnpm exec eslint",
+ "test": "vitest",
+ "setup": "pnpm install && pnpm run setup:hooks",
+ "setup:hooks": "pnpm exec husky install",
+ "format": "prettier --write \"{Components,Containers,helpers,pages,styles}/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
+ },
+ "dependencies": {
+ "@tabler/icons-react": "^3.37.1",
+ "async-mutex": "^0.5.0",
+ "bcryptjs": "^3.0.3",
+ "chart.js": "^4.5.1",
+ "date-fns": "^4.1.0",
+ "lowdb": "^7.0.1",
+ "next": "^16.1.6",
+ "next-auth": "^4.24.13",
+ "nodemailer": "^8.0.1",
+ "nprogress": "^0.2.0",
+ "react": "^19.2.4",
+ "react-chartjs-2": "^5.3.1",
+ "react-dom": "^19.2.4",
+ "react-hook-form": "^7.71.2",
+ "react-select": "^5.10.2",
+ "react-toastify": "^11.0.5",
+ "swr": "^2.4.1",
+ "use-media": "^1.5.0",
+ "uuid": "^13.0.0"
+ },
+ "devDependencies": {
+ "@commitlint/cli": "^20.4.2",
+ "@commitlint/config-conventional": "^20.4.2",
+ "@types/node": "^25.3.3",
+ "@types/nodemailer": "^7.0.11",
+ "@types/nprogress": "^0.2.3",
+ "@types/react": "^19.2.14",
+ "@types/supertest": "^7.2.0",
+ "eslint": "^9.39.3",
+ "eslint-config-next": "^16.1.6",
+ "husky": "^9.1.7",
+ "node-mocks-http": "^1.17.2",
+ "prettier": "^3.8.1",
+ "typescript": "^5.9.3",
+ "vitest": "^4.0.18"
+ }
}
diff --git a/pages/404.js b/pages/404.js
deleted file mode 100644
index ba7511a..0000000
--- a/pages/404.js
+++ /dev/null
@@ -1,37 +0,0 @@
-//Lib
-import Head from 'next/head';
-import { useSession } from 'next-auth/react';
-import { useRouter } from 'next/router';
-import Image from 'next/image';
-
-export default function Error404() {
- //Var
- const { status } = useSession();
- const router = useRouter();
-
- if (status === 'authenticated') {
- return (
- <>
-
- 404 - Page not found
-
-
-
-
- >
- );
- } else {
- router.replace('/login');
- }
-}
diff --git a/pages/404.tsx b/pages/404.tsx
new file mode 100644
index 0000000..0676e2f
--- /dev/null
+++ b/pages/404.tsx
@@ -0,0 +1,31 @@
+import Head from 'next/head';
+import { useSession } from 'next-auth/react';
+import { useRouter } from 'next/router';
+import Image from 'next/image';
+
+export default function Error404() {
+ const { status } = useSession();
+ const router = useRouter();
+
+ if (status === 'authenticated') {
+ return (
+ <>
+
+ 404 - Page not found
+
+
+
+
+ >
+ );
+ } else {
+ router.replace('/login');
+ }
+}
diff --git a/pages/_app.js b/pages/_app.js
deleted file mode 100644
index a553a82..0000000
--- a/pages/_app.js
+++ /dev/null
@@ -1,28 +0,0 @@
-//Lib
-import '../styles/default.css';
-import Head from 'next/head';
-import { ToastContainer } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-import { SessionProvider } from 'next-auth/react';
-
-//Components
-import Layout from '../Components/UI/Layout/Layout';
-
-export default function MyApp({ Component, pageProps }) {
- return (
-
-
-
-
-
- BorgWarehouse
-
-
-
-
-
- );
-}
diff --git a/pages/_app.tsx b/pages/_app.tsx
new file mode 100644
index 0000000..8fde42f
--- /dev/null
+++ b/pages/_app.tsx
@@ -0,0 +1,36 @@
+import '../styles/default.css';
+import Head from 'next/head';
+import { ToastContainer } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import { SessionProvider } from 'next-auth/react';
+import { AppProps } from 'next/app';
+import Router from 'next/router';
+import NProgress from 'nprogress';
+
+//Components
+import Layout from '../Components/UI/Layout/Layout';
+import { LoaderProvider } from '~/contexts/LoaderContext';
+
+NProgress.configure({ showSpinner: false });
+
+Router.events.on('routeChangeStart', () => NProgress.start());
+Router.events.on('routeChangeComplete', () => NProgress.done());
+Router.events.on('routeChangeError', () => NProgress.done());
+
+export default function MyApp({ Component, pageProps }: AppProps) {
+ return (
+
+
+
+
+
+
+ BorgWarehouse
+
+
+
+
+
+
+ );
+}
diff --git a/pages/account/index.js b/pages/account/index.js
deleted file mode 100644
index 2593fef..0000000
--- a/pages/account/index.js
+++ /dev/null
@@ -1,50 +0,0 @@
-//Lib
-import Head from 'next/head';
-import 'react-toastify/dist/ReactToastify.css';
-import { useSession } from 'next-auth/react';
-import { authOptions } from '../../pages/api/auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-//Components
-import UserSettings from '../../Containers/UserSettings/UserSettings';
-
-export default function Account() {
- ////Var
- const { status, data } = useSession();
-
- //Function
- if (status == 'unauthenticated' || status == 'loading') {
- return Loading...
;
- }
- return (
- <>
-
- Account - BorgWarehouse
-
-
-
- >
- );
-}
-
-export async function getServerSideProps(context) {
- //Var
- const session = await getServerSession(
- context.req,
- context.res,
- authOptions
- );
-
- if (!session) {
- return {
- redirect: {
- destination: '/login',
- permanent: false,
- },
- };
- }
-
- return {
- props: {},
- };
-}
diff --git a/pages/account/index.tsx b/pages/account/index.tsx
new file mode 100644
index 0000000..000b4a6
--- /dev/null
+++ b/pages/account/index.tsx
@@ -0,0 +1,44 @@
+import Head from 'next/head';
+import 'react-toastify/dist/ReactToastify.css';
+import { useSession } from 'next-auth/react';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { GetServerSidePropsContext } from 'next';
+import { SessionStatus } from '~/types';
+
+//Components
+import UserSettings from '~/Containers/UserSettings/UserSettings';
+
+export default function Account() {
+ const { status, data } = useSession();
+
+ if (status == 'unauthenticated' || status == 'loading' || !data) {
+ return Loading...
;
+ }
+ return (
+ <>
+
+ Account - BorgWarehouse
+
+
+
+ >
+ );
+}
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ if (!session) {
+ return {
+ redirect: {
+ destination: '/login',
+ permanent: false,
+ },
+ };
+ }
+
+ return {
+ props: {},
+ };
+}
diff --git a/pages/api/account/getAppriseAlert.js b/pages/api/account/getAppriseAlert.js
deleted file mode 100644
index 9254934..0000000
--- a/pages/api/account/getAppriseAlert.js
+++ /dev/null
@@ -1,63 +0,0 @@
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'GET') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
- try {
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- } else {
- //Send the appriseAlert bool
- res.status(200).json({
- appriseAlert: usersList[userIndex].appriseAlert,
- });
- return;
- }
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/getAppriseMode.js b/pages/api/account/getAppriseMode.js
deleted file mode 100644
index efd5b32..0000000
--- a/pages/api/account/getAppriseMode.js
+++ /dev/null
@@ -1,65 +0,0 @@
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'GET') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
- try {
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- } else {
- //Send the appriseMode object
- res.status(200).json({
- appriseMode: usersList[userIndex].appriseMode,
- appriseStatelessURL:
- usersList[userIndex].appriseStatelessURL,
- });
- return;
- }
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/getAppriseServices.js b/pages/api/account/getAppriseServices.js
deleted file mode 100644
index 56ce6ed..0000000
--- a/pages/api/account/getAppriseServices.js
+++ /dev/null
@@ -1,63 +0,0 @@
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'GET') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
- try {
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- } else {
- //Send the appriseServices array
- res.status(200).json({
- appriseServices: usersList[userIndex].appriseServices,
- });
- return;
- }
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/getEmailAlert.js b/pages/api/account/getEmailAlert.js
deleted file mode 100644
index fe22025..0000000
--- a/pages/api/account/getEmailAlert.js
+++ /dev/null
@@ -1,63 +0,0 @@
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'GET') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
- try {
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- } else {
- //Send the emailAlert bool
- res.status(200).json({
- emailAlert: usersList[userIndex].emailAlert,
- });
- return;
- }
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/getWizardEnv.js b/pages/api/account/getWizardEnv.js
deleted file mode 100644
index 8a6d618..0000000
--- a/pages/api/account/getWizardEnv.js
+++ /dev/null
@@ -1,49 +0,0 @@
-//Lib
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'GET') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
- try {
- function getEnvVariable(envName, defaultValue = '') {
- return process.env[envName] || defaultValue;
- }
-
- const wizardEnv = {
- UNIX_USER: getEnvVariable('UNIX_USER', 'borgwarehouse'),
- FQDN: getEnvVariable('FQDN', 'localhost'),
- SSH_SERVER_PORT: getEnvVariable('SSH_SERVER_PORT', '22'),
- FQDN_LAN: getEnvVariable('FQDN_LAN'),
- SSH_SERVER_PORT_LAN: getEnvVariable('SSH_SERVER_PORT_LAN'),
- SSH_SERVER_FINGERPRINT_RSA: getEnvVariable(
- 'SSH_SERVER_FINGERPRINT_RSA'
- ),
- SSH_SERVER_FINGERPRINT_ED25519: getEnvVariable(
- 'SSH_SERVER_FINGERPRINT_ED25519'
- ),
- SSH_SERVER_FINGERPRINT_ECDSA: getEnvVariable(
- 'SSH_SERVER_FINGERPRINT_ECDSA'
- ),
- };
- res.status(200).json({ wizardEnv });
- return;
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/sendTestApprise.js b/pages/api/account/sendTestApprise.js
deleted file mode 100644
index 66e7b27..0000000
--- a/pages/api/account/sendTestApprise.js
+++ /dev/null
@@ -1,163 +0,0 @@
-//Lib
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-import { promises as fs } from 'fs';
-import path from 'path';
-const { exec } = require('child_process');
-
-export default async function handler(req, res) {
- if (req.method == 'POST') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- let { sendTestApprise } = req.body;
-
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //1 : Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- }
-
- //2 : control the data
- if (sendTestApprise !== true) {
- res.status(422).json({ message: 'Unexpected data' });
- return;
- }
-
- //3 : if there is no service URLs, throw error
- if (
- !usersList[userIndex].appriseServices ||
- usersList[userIndex].appriseServices.length === 0
- ) {
- res.status(422).json({
- message:
- 'You must provide at least one Apprise URL to send a test.',
- });
- return;
- }
-
- ////4 : Send the notification to services
- //Build the URLs service list as a single string
- let appriseServicesURLs = '';
- for (let service of usersList[userIndex].appriseServices) {
- appriseServicesURLs = appriseServicesURLs + service + ' ';
- }
- //Mode : package
- if (usersList[userIndex].appriseMode === 'package') {
- try {
- //Check if apprise is installed as local package.
- exec('apprise -V', (error, stderr, stdout) => {
- if (error) {
- console.log(
- `Error when checking if Apprise is a local package : ${error}`
- );
- res.status(500).json({
- message:
- 'Apprise is not installed as local package on your server.',
- });
- return;
- } else {
- //Send notification via local package.
- exec(
- `apprise -v -b "This is a test notification from BorgWarehouse !" ${appriseServicesURLs}`,
- (error, stderr, stdout) => {
- if (stderr) {
- res.status(500).json({
- message:
- 'There are some errors : ' + stderr,
- });
- return;
- } else {
- res.status(200).json({
- message:
- 'Notifications successfully sent.',
- });
- return;
- }
- }
- );
- }
- });
- } catch (err) {
- console.log(err);
- res.status(500).json({
- message:
- 'Error on sending notification. Contact your administrator.',
- });
- return;
- }
-
- //Mode : stateless
- } else if (usersList[userIndex].appriseMode === 'stateless') {
- //If stateless URL is empty
- if (usersList[userIndex].appriseStatelessURL === '') {
- res.status(500).json({
- message: 'Please, provide an Apprise stateless API URL.',
- });
- return;
- }
- try {
- await fetch(
- usersList[userIndex].appriseStatelessURL + '/notify',
- {
- method: 'POST',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify({
- urls: appriseServicesURLs,
- body: 'This is a test notification from BorgWarehouse !',
- }),
- }
- ).then((response) => {
- if (response.ok) {
- res.status(200).json({
- message: 'Notifications successfully sent.',
- });
- return;
- } else {
- console.log(response);
- res.status(500).json({
- message:
- 'There are some errors : ' +
- response.statusText,
- });
- return;
- }
- });
- } catch (err) {
- res.status(500).json({
- message: 'Error : ' + err.message,
- });
- return;
- }
-
- //Mode : unknown
- } else {
- res.status(422).json({
- message: 'No Apprise Mode selected or supported.',
- });
- }
- }
-}
diff --git a/pages/api/account/sendTestEmail.js b/pages/api/account/sendTestEmail.js
deleted file mode 100644
index 3bf0ea9..0000000
--- a/pages/api/account/sendTestEmail.js
+++ /dev/null
@@ -1,48 +0,0 @@
-//Lib
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-import nodemailerSMTP from '../../../helpers/functions/nodemailerSMTP';
-import emailTest from '../../../helpers/templates/emailTest';
-
-export default async function handler(req, res) {
- if (req.method == 'POST') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //Create the SMTP Transporter
- const transporter = nodemailerSMTP();
-
- //Mail options
- const mailData = emailTest(session.user.email, session.user.name);
-
- //Send mail
- try {
- transporter.sendMail(mailData, function (err, info) {
- if (err) {
- console.log(err);
- res.status(400).json({
- message:
- 'An error occured while sending the email : ' + err,
- });
- return;
- } else {
- console.log(info);
- res.status(200).json({
- message: 'Mail successfully sent.',
- });
- return;
- }
- });
- } catch (err) {
- console.log(err);
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator.',
- });
- }
- }
-}
diff --git a/pages/api/account/updateAppriseAlert.js b/pages/api/account/updateAppriseAlert.js
deleted file mode 100644
index 832e229..0000000
--- a/pages/api/account/updateAppriseAlert.js
+++ /dev/null
@@ -1,86 +0,0 @@
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'PUT') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- let { appriseAlert } = req.body;
-
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //1 : control the data
- if (typeof appriseAlert != 'boolean') {
- res.status(422).json({ message: 'Unexpected data' });
- return;
- }
-
- //2 : Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- }
-
- //3 : Change the appriseAlert settings
- try {
- //Modify the appriseAlert bool for the user
- let newUsersList = usersList.map((user) =>
- user.username == session.user.name
- ? { ...user, appriseAlert: appriseAlert }
- : user
- );
- //Stringify the new users list
- newUsersList = JSON.stringify(newUsersList);
- //Write the new JSON
- await fs.writeFile(
- jsonDirectory + '/users.json',
- newUsersList,
- (err) => {
- if (err) console.log(err);
- }
- );
- res.status(200).json({ message: 'Successful API send' });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/updateAppriseMode.js b/pages/api/account/updateAppriseMode.js
deleted file mode 100644
index 9c826eb..0000000
--- a/pages/api/account/updateAppriseMode.js
+++ /dev/null
@@ -1,90 +0,0 @@
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'PUT') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- let { appriseMode, appriseStatelessURL } = req.body;
-
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //1 : control the data
- if (appriseMode != 'package' && appriseMode != 'stateless') {
- res.status(422).json({ message: 'Unexpected data' });
- return;
- }
-
- //2 : Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- }
-
- //3 : Change the appriseMode
- try {
- //Modify the appriseMode for the user
- let newUsersList = usersList.map((user) =>
- user.username == session.user.name
- ? {
- ...user,
- appriseMode: appriseMode,
- appriseStatelessURL: appriseStatelessURL,
- }
- : user
- );
- //Stringify the new users list
- newUsersList = JSON.stringify(newUsersList);
- //Write the new JSON
- await fs.writeFile(
- jsonDirectory + '/users.json',
- newUsersList,
- (err) => {
- if (err) console.log(err);
- }
- );
- res.status(200).json({ message: 'Successful API send' });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/updateAppriseServices.js b/pages/api/account/updateAppriseServices.js
deleted file mode 100644
index 83027bf..0000000
--- a/pages/api/account/updateAppriseServices.js
+++ /dev/null
@@ -1,89 +0,0 @@
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'PUT') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- let { appriseURLs } = req.body;
-
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //1 : Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- }
-
- //2 : Update Apprise URLs list
- try {
- //Build the services URLs list from form
- const appriseURLsArray = appriseURLs
- .replace(/ /g, '')
- .split('\n')
- .filter((el) => el != '');
-
- //Save the list for the user
- let newUsersList = usersList.map((user) =>
- user.username == session.user.name
- ? {
- ...user,
- appriseServices: appriseURLsArray,
- }
- : user
- );
- //Stringify the new users list
- newUsersList = JSON.stringify(newUsersList);
- //Write the new JSON
- await fs.writeFile(
- jsonDirectory + '/users.json',
- newUsersList,
- (err) => {
- if (err) console.log(err);
- }
- );
- res.status(200).json({ message: 'Successful API send' });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/updateEmail.js b/pages/api/account/updateEmail.js
deleted file mode 100644
index 33ff5ed..0000000
--- a/pages/api/account/updateEmail.js
+++ /dev/null
@@ -1,96 +0,0 @@
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'PUT') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- let { email } = req.body;
-
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //1 : We check that we receive data.
- if (!email) {
- //If a variable is empty.
- res.status(400).json({ message: 'A field is missing.' });
- return;
- }
-
- //2 : control the data
- const emailRegex = new RegExp(
- /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
- );
- if (!emailRegex.test(email)) {
- res.status(400).json({ message: 'Your email is not valid' });
- return;
- }
-
- //3 : Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- }
-
- //4 : Change the email
- try {
- //Modify the email for the user
- let newUsersList = usersList.map((user) =>
- user.username == session.user.name
- ? { ...user, email: email }
- : user
- );
- //Stringify the new users list
- newUsersList = JSON.stringify(newUsersList);
- //Write the new JSON
- await fs.writeFile(
- jsonDirectory + '/users.json',
- newUsersList,
- (err) => {
- if (err) console.log(err);
- }
- );
- res.status(200).json({ message: 'Successful API send' });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/updateEmailAlert.js b/pages/api/account/updateEmailAlert.js
deleted file mode 100644
index fa8b28b..0000000
--- a/pages/api/account/updateEmailAlert.js
+++ /dev/null
@@ -1,86 +0,0 @@
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'PUT') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- let { emailAlert } = req.body;
-
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //1 : control the data
- if (typeof emailAlert != 'boolean') {
- res.status(422).json({ message: 'Unexpected data' });
- return;
- }
-
- //2 : Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- }
-
- //3 : Change the emailAlert settings
- try {
- //Modify the email for the user
- let newUsersList = usersList.map((user) =>
- user.username == session.user.name
- ? { ...user, emailAlert: emailAlert }
- : user
- );
- //Stringify the new users list
- newUsersList = JSON.stringify(newUsersList);
- //Write the new JSON
- await fs.writeFile(
- jsonDirectory + '/users.json',
- newUsersList,
- (err) => {
- if (err) console.log(err);
- }
- );
- res.status(200).json({ message: 'Successful API send' });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/updatePassword.js b/pages/api/account/updatePassword.js
deleted file mode 100644
index 7359a63..0000000
--- a/pages/api/account/updatePassword.js
+++ /dev/null
@@ -1,95 +0,0 @@
-//Lib
-import { hashPassword, verifyPassword } from '../../../helpers/functions/auth';
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'PUT') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- let { oldPassword, newPassword } = req.body;
-
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //1 : We check that we receive data for each variable.
- if (!oldPassword || !newPassword) {
- //If a variable is empty.
- res.status(400).json({ message: 'A field is missing.' });
- return;
- }
- //Hash the new password
- newPassword = await hashPassword(newPassword);
-
- //2 : Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({ message: 'User is incorrect.' });
- return;
- }
- const user = usersList[userIndex];
-
- //3 : Check that the old password is correct
- const isValid = await verifyPassword(oldPassword, user.password);
- if (!isValid) {
- res.status(400).json({ message: 'Old password is incorrect.' });
- return;
- }
-
- //4 : Change the password
- try {
- //Modify the password for the user
- let newUsersList = usersList.map((user) =>
- user.username == session.user.name
- ? { ...user, password: newPassword }
- : user
- );
- //Stringify the new users list
- newUsersList = JSON.stringify(newUsersList);
- //Write the new JSON
- await fs.writeFile(
- jsonDirectory + '/users.json',
- newUsersList,
- (err) => {
- if (err) console.log(err);
- }
- );
- res.status(200).json({ message: 'Successful API send' });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/account/updateUsername.js b/pages/api/account/updateUsername.js
deleted file mode 100644
index 2d158d7..0000000
--- a/pages/api/account/updateUsername.js
+++ /dev/null
@@ -1,96 +0,0 @@
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'PUT') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- let { username } = req.body;
-
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //1 : We check that we receive data.
- if (!username) {
- //If a variable is empty.
- res.status(400).json({ message: 'A field is missing.' });
- return;
- }
-
- //2 : control the data
- const usernameRegex = new RegExp(/^[a-z]{5,15}$/);
- if (!usernameRegex.test(username)) {
- res.status(400).json({
- message: 'Only a-z characters are allowed (5 to 15 char.)',
- });
- return;
- }
-
- //3 : Verify that the user of the session exists
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(session.user.name);
- if (userIndex === -1) {
- res.status(400).json({
- message:
- 'User is incorrect. Please, logout to update your session.',
- });
- return;
- }
-
- //4 : Change the username
- try {
- //Modify the username for the user
- let newUsersList = usersList.map((user) =>
- user.username == session.user.name
- ? { ...user, username: username }
- : user
- );
- //Stringify the new users list
- newUsersList = JSON.stringify(newUsersList);
- //Write the new JSON
- await fs.writeFile(
- jsonDirectory + '/users.json',
- newUsersList,
- (err) => {
- if (err) console.log(err);
- }
- );
- res.status(200).json({ message: 'Successful API send' });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({ message: 'Bad request on API' });
- }
-}
diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js
deleted file mode 100644
index 1dcad01..0000000
--- a/pages/api/auth/[...nextauth].js
+++ /dev/null
@@ -1,105 +0,0 @@
-//Lib
-import NextAuth from 'next-auth';
-import CredentialsProvider from 'next-auth/providers/credentials';
-import { verifyPassword } from '../../../helpers/functions/auth';
-import fs from 'fs';
-import path from 'path';
-
-const logLogin = async (message, req, success = false) => {
- const ipAddress = req.headers['x-forwarded-for'] || 'unknown';
- if (success) {
- console.log(`Login success from ${ipAddress} with user ${message}`);
- } else {
- console.log(`Login failed from ${ipAddress} : ${message}`);
- }
-};
-
-////Use if need getServerSideProps and therefore getServerSession
-export const authOptions = {
- providers: [
- CredentialsProvider({
- async authorize(credentials, req) {
- const { username, password } = credentials;
- //Read the users file
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- //Check if the users.json file exists and initialize it if not with admin/admin.
- if (!fs.existsSync(jsonDirectory + '/users.json')) {
- fs.writeFileSync(
- jsonDirectory + '/users.json',
- JSON.stringify([
- {
- id: 0,
- email: '',
- username: 'admin',
- password:
- '$2a$12$20yqRnuaDBH6AE0EvIUcEOzqkuBtn1wDzJdw2Beg8w9S.vEqdso0a',
- roles: ['admin'],
- emailAlert: false,
- appriseAlert: false,
- },
- ])
- );
- }
- let usersList = await fs.promises.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
-
- //Step 1 : does the user exist ?
- const userIndex = usersList
- .map((user) => user.username)
- .indexOf(username.toLowerCase());
- if (userIndex === -1) {
- await logLogin(`Bad username ${req.body.username}`, req);
- throw new Error('Incorrect credentials.');
- }
- const user = usersList[userIndex];
-
- //Step 2 : Is the password correct ?
- const isValid = await verifyPassword(password, user.password);
- if (!isValid) {
- await logLogin(
- `Wrong password for ${req.body.username}`,
- req
- );
- throw new Error('Incorrect credentials.');
- }
-
- //Success
- const account = {
- name: user.username,
- email: user.email,
- id: user.id,
- roles: user.roles,
- };
-
- await logLogin(req.body.username, req, true);
- return account;
- },
- }),
- ],
- callbacks: {
- async jwt({ token, user }) {
- // Persist the role and the ID to the token right after signin. "user" is the response from signin, and we return account.
- if (user) {
- token.roles = user.roles;
- token.id = user.id;
- }
- return token;
- },
- async session({ session, token }) {
- // Send properties to the client to access to the token info through session().
- if (token) {
- session.user.roles = token.roles;
- session.user.id = token.id;
- }
- return session;
- },
- },
- secret: process.env.NEXTAUTH_SECRET,
-};
-
-export default NextAuth(authOptions);
diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts
new file mode 100644
index 0000000..710e9e3
--- /dev/null
+++ b/pages/api/auth/[...nextauth].ts
@@ -0,0 +1,106 @@
+import fs from 'fs';
+import NextAuth, { NextAuthOptions, RequestInternal, User } from 'next-auth';
+import CredentialsProvider from 'next-auth/providers/credentials';
+import path from 'path';
+import { ConfigService, AuthService } from '~/services';
+
+const logLogin = async (message: string, req: Partial, success = false) => {
+ const ipAddress = req.headers?.['x-forwarded-for'] || 'unknown';
+ const timestamp = new Date().toISOString();
+ if (success) {
+ console.log(`Login success from ${ipAddress} with user ${message} [${timestamp}]`);
+ } else {
+ console.log(`Login failed from ${ipAddress} : ${message} [${timestamp}]`);
+ }
+};
+
+interface customUser extends User {
+ roles: string[];
+}
+
+////Use if need getServerSideProps and therefore getServerSession
+export const authOptions: NextAuthOptions = {
+ providers: [
+ CredentialsProvider({
+ credentials: {
+ username: { type: 'text' },
+ password: { type: 'password' },
+ },
+ async authorize(credentials, req) {
+ if (!credentials) {
+ throw new Error('Missing credentials');
+ }
+ const { username, password } = credentials;
+ //Read the users file
+ //Find the absolute path of the json directory
+ const jsonDirectory = path.join(process.cwd(), '/config');
+ //Check if the users.json file exists and initialize it if not with admin/admin.
+ if (!fs.existsSync(jsonDirectory + '/users.json')) {
+ fs.writeFileSync(
+ jsonDirectory + '/users.json',
+ JSON.stringify([
+ {
+ id: 0,
+ email: '',
+ username: 'admin',
+ password: '$2a$12$20yqRnuaDBH6AE0EvIUcEOzqkuBtn1wDzJdw2Beg8w9S.vEqdso0a',
+ roles: ['admin'],
+ emailAlert: false,
+ appriseAlert: false,
+ },
+ ])
+ );
+ }
+
+ const usersList = await ConfigService.getUsersList();
+
+ //Step 1 : does the user exist ?
+ const userIndex = usersList.map((user) => user.username).indexOf(username.toLowerCase());
+ if (userIndex === -1) {
+ await logLogin(`Bad username ${req.body?.username}`, req);
+ throw new Error('Incorrect credentials.');
+ }
+ const user = usersList[userIndex];
+
+ //Step 2 : Is the password correct ?
+ const isValid = await AuthService.verifyPassword(password, user.password);
+ if (!isValid) {
+ await logLogin(`Wrong password for ${req.body?.username}`, req);
+ throw new Error('Incorrect credentials.');
+ }
+
+ //Success
+ const account: customUser = {
+ name: user.username,
+ email: user.email,
+ id: user.id.toString(),
+ roles: user.roles,
+ };
+
+ await logLogin(req.body?.username, req, true);
+ return account;
+ },
+ }),
+ ],
+ callbacks: {
+ async jwt({ token, user }) {
+ // Persist the role and the ID to the token right after signin. "user" is the response from signin, and we return account.
+ if (user) {
+ token.roles = user.roles;
+ token.id = user.id;
+ }
+ return token;
+ },
+ async session({ session, token }) {
+ // Send properties to the client to access to the token info through session().
+ if (token && session.user) {
+ session.user.roles = token.roles as string[];
+ session.user.id = token.id as string;
+ }
+ return session;
+ },
+ },
+ secret: process.env.NEXTAUTH_SECRET,
+};
+
+export default NextAuth(authOptions);
diff --git a/pages/api/cronjob/checkStatus.js b/pages/api/cronjob/checkStatus.js
deleted file mode 100644
index 842aaa3..0000000
--- a/pages/api/cronjob/checkStatus.js
+++ /dev/null
@@ -1,248 +0,0 @@
-// This API is design to be used by a cron (of your choice). Call it with curl for example
-//(e.g : curl --request POST --url 'http://localhost:3000/api/cronjob/checkStatus' --header 'Authorization: Bearer 5173f388c0f4a0df92d1412c3036ddc897c22e4448')
-
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-const util = require('node:util');
-const exec = util.promisify(require('node:child_process').exec);
-import nodemailerSMTP from '../../../helpers/functions/nodemailerSMTP';
-import emailAlertStatus from '../../../helpers/templates/emailAlertStatus';
-
-export default async function handler(req, res) {
- if (req.headers.authorization == null) {
- res.status(401).json({
- status: 401,
- message: 'Unauthorized',
- });
- return;
- }
-
- const CRONJOB_KEY = process.env.CRONJOB_KEY;
- const ACTION_KEY = req.headers.authorization.split(' ')[1];
-
- if (req.method == 'POST' && ACTION_KEY === CRONJOB_KEY) {
- //Var
- let newRepoList;
- let repoListToSendAlert = [];
- let usersList;
- const date = Math.round(Date.now() / 1000);
- const jsonDirectory = path.join(process.cwd(), '/config');
-
- ////PART 1 : Status
- try {
- //Check if there are some repositories
- let repoList = await fs.readFile(
- jsonDirectory + '/repo.json',
- 'utf8'
- );
- repoList = JSON.parse(repoList);
- if (repoList.length === 0) {
- res.status(200).json({
- success:
- 'Status cron has been executed. No repository to check.',
- });
- return;
- }
-
- //Call the shell : getLastSave.sh
- //Find the absolute path of the shells directory
- const shellsDirectory = path.join(process.cwd(), '/helpers');
- //Exec the shell
- const { stdout, stderr } = await exec(
- `${shellsDirectory}/shells/getLastSave.sh`
- );
- if (stderr) {
- console.log('stderr:', stderr);
- res.status(500).json({
- status: 500,
- message:
- 'Error on getting the date for last save, contact the administrator.',
- });
- return;
- }
- //Parse the JSON output of getLastSave.sh to use it
- const lastSave = JSON.parse(stdout);
-
- //Rebuild a newRepoList with the lastSave timestamp updated and the status updated.
- newRepoList = repoList;
- for (let index in newRepoList) {
- const repoFiltered = lastSave.filter(
- (x) =>
- x.repositoryName === newRepoList[index].repositoryName
- );
- if (repoFiltered.length === 1) {
- //Write the timestamp of the last save
- newRepoList[index].lastSave = repoFiltered[0].lastSave;
- //Trigger the status if the last save is older than alert setting.
- if (
- date - newRepoList[index].lastSave >
- newRepoList[index].alert
- ) {
- newRepoList[index].status = false;
- } else if (
- date - newRepoList[index].lastSave <
- newRepoList[index].alert
- ) {
- newRepoList[index].status = true;
- }
- }
- }
- } catch (err) {
- res.status(500).json({
- status: 500,
- message: "API error : can't update the status.",
- });
- return;
- }
-
- //// PART 2 : check if there is a repo that need an alert
- try {
- //Here, a mail is sent every 24H (90000) if a repo has down status
- for (let index in newRepoList) {
- if (
- !newRepoList[index].status &&
- newRepoList[index].alert !== 0 &&
- (!newRepoList[index].lastStatusAlertSend ||
- date - newRepoList[index].lastStatusAlertSend > 90000)
- ) {
- repoListToSendAlert.push(newRepoList[index].alias);
- newRepoList[index].lastStatusAlertSend = date;
- }
- }
- } catch (err) {
- res.status(500).json({
- status: 500,
- message:
- "API error : can't check if a repo needs an email alert.",
- });
- return;
- }
-
- //PART 3 : Save the new repoList
- try {
- //Stringify the repoList to write it into the json file.
- newRepoList = JSON.stringify(newRepoList);
- //Write the new json
- await fs.writeFile(
- jsonDirectory + '/repo.json',
- newRepoList,
- (err) => {
- if (err) console.log(err);
- }
- );
- } catch (err) {
- res.status(500).json({
- status: 500,
- message: "API error : can't write the new repoList.",
- });
- return;
- }
- //PART 4 : Send the alerts
- if (repoListToSendAlert.length > 0) {
- // Read user informations
- try {
- //Read the email of the user
- usersList = await fs.readFile(
- jsonDirectory + '/users.json',
- 'utf8'
- );
- //Parse the usersList
- usersList = JSON.parse(usersList);
- } catch (err) {
- res.status(500).json({
- status: 500,
- message: "API error : can't read user information.",
- });
- return;
- }
- ////EMAIL
- // If the user has enabled email alerts
- if (usersList[0].emailAlert) {
- //Send mail
- //Create the SMTP Transporter
- const transporter = nodemailerSMTP();
- //Mail options
- const mailData = emailAlertStatus(
- usersList[0].email,
- usersList[0].username,
- repoListToSendAlert
- );
- transporter.sendMail(mailData, function (err, info) {
- if (err) {
- console.log(err);
- } else {
- console.log(info);
- }
- });
- }
- ////APPRISE
- // If the user has enabled Apprise alerts
- if (usersList[0].appriseAlert) {
- let appriseServicesURLs = '';
- for (let service of usersList[0].appriseServices) {
- appriseServicesURLs = appriseServicesURLs + service + ' ';
- }
- //Mode : package
- if (usersList[0].appriseMode === 'package') {
- try {
- //Send notification via local package.
- await exec(
- `apprise -v -b '๐ด Some repositories on BorgWarehouse need attention !\nList of down repositories :\n ${repoListToSendAlert}' ${appriseServicesURLs}`
- );
- } catch (err) {
- console.log(err.stderr);
- res.status(500).json({
- message: 'Error : ' + err.stderr,
- });
- return;
- }
-
- //Mode : stateless
- } else if (usersList[0].appriseMode === 'stateless') {
- try {
- await fetch(
- usersList[0].appriseStatelessURL + '/notify',
- {
- method: 'POST',
- headers: {
- 'Content-type': 'application/json',
- },
- body: JSON.stringify({
- urls: appriseServicesURLs,
- body:
- '๐ด Some repositories on BorgWarehouse need attention !\nList of down repositories :\n' +
- repoListToSendAlert,
- }),
- }
- );
- } catch (err) {
- console.log(err);
- res.status(500).json({
- message: 'Error : ' + err.message,
- });
- return;
- }
-
- //Mode : unknown
- } else {
- res.status(422).json({
- message: 'No Apprise Mode selected or supported.',
- });
- }
- }
- }
-
- //PART 5 : Sucess
- res.status(200).json({
- success: 'Status cron has been executed.',
- });
- return;
- } else {
- res.status(401).json({
- status: 401,
- message: 'Unauthorized',
- });
- return;
- }
-}
diff --git a/pages/api/cronjob/getStorageUsed.js b/pages/api/cronjob/getStorageUsed.js
deleted file mode 100644
index 16cf508..0000000
--- a/pages/api/cronjob/getStorageUsed.js
+++ /dev/null
@@ -1,96 +0,0 @@
-// This API is design to be used by a cron (of your choice). Call it with curl for example
-//(e.g : curl --request POST --url 'http://localhost:3000/api/cronjob/getStorageUsed' --header 'Authorization: Bearer 5173f388c0f4a0df92d1412c3036ddc897c22e4448')
-
-//Lib
-import { promises as fs } from 'fs';
-import path from 'path';
-const util = require('node:util');
-const exec = util.promisify(require('node:child_process').exec);
-
-export default async function handler(req, res) {
- if (req.headers.authorization == null) {
- res.status(401).json({
- status: 401,
- message: 'Unauthorized',
- });
- return;
- }
-
- const CRONJOB_KEY = process.env.CRONJOB_KEY;
- const ACTION_KEY = req.headers.authorization.split(' ')[1];
-
- try {
- if (req.method == 'POST' && ACTION_KEY === CRONJOB_KEY) {
- //Check the repoList
- const jsonDirectory = path.join(process.cwd(), '/config');
- let repoList = await fs.readFile(
- jsonDirectory + '/repo.json',
- 'utf8'
- );
- //Parse the repoList
- repoList = JSON.parse(repoList);
- //If repoList is empty we stop here.
- if (repoList.length === 0) {
- res.status(200).json({
- success: 'No repositories to analyse yet.',
- });
- return;
- }
-
- ////Call the shell : getStorageUsed.sh
- //Find the absolute path of the shells directory
- const shellsDirectory = path.join(process.cwd(), '/helpers');
- //Exec the shell
- const { stdout, stderr } = await exec(
- `${shellsDirectory}/shells/getStorageUsed.sh`
- );
- if (stderr) {
- res.status(500).json({
- status: 500,
- message:
- 'Error on getting storage, contact the administrator.',
- });
- return;
- }
- //Parse the JSON output of getStorageUsed.sh to use it
- const storageUsed = JSON.parse(stdout);
-
- //Rebuild a newRepoList with the storageUsed value updated
- let newRepoList = repoList;
- for (let index in newRepoList) {
- const repoFiltered = storageUsed.filter(
- (x) => x.name === newRepoList[index].repositoryName
- );
- if (repoFiltered.length === 1) {
- newRepoList[index].storageUsed = repoFiltered[0].size;
- }
- }
-
- //Stringify the repoList to write it into the json file.
- newRepoList = JSON.stringify(newRepoList);
- //Write the new json
- await fs.writeFile(
- jsonDirectory + '/repo.json',
- newRepoList,
- (err) => {
- if (err) console.log(err);
- }
- );
-
- res.status(200).json({
- success: 'Storage cron has been executed.',
- });
- } else {
- res.status(401).json({
- status: 401,
- message: 'Unauthorized',
- });
- }
- } catch (err) {
- console.log(err);
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator.',
- });
- }
-}
diff --git a/pages/api/repo/add.js b/pages/api/repo/add.js
deleted file mode 100644
index 6359920..0000000
--- a/pages/api/repo/add.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../../../pages/api/auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-import repoHistory from '../../../helpers/functions/repoHistory';
-const util = require('node:util');
-const exec = util.promisify(require('node:child_process').exec);
-
-export default async function handler(req, res) {
- if (req.method == 'POST') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- const {
- alias,
- sshPublicKey,
- size,
- comment,
- alert,
- lanCommand,
- appendOnlyMode,
- } = req.body;
- //We check that we receive data for each variable. Only "comment" and "lanCommand" are optional in the form.
- if (
- !alias ||
- !sshPublicKey ||
- !size ||
- typeof appendOnlyMode !== 'boolean' ||
- (!alert && alert !== 0)
- ) {
- //If a variable is empty.
- res.status(422).json({
- message: 'Unexpected data',
- });
- //A return to make sure we don't go any further if data are incorrect.
- return;
- }
-
- try {
- //console.log('API call (PUT)');
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let repoList = await fs.readFile(
- jsonDirectory + '/repo.json',
- 'utf8'
- );
- //Parse the repoList
- repoList = JSON.parse(repoList);
-
- //Find the first biggest ID available to assign it, so the highest ID is already the last added.
- let newID = 0;
- for (let element in repoList) {
- if (newID <= repoList[element].id) {
- newID = repoList[element].id + 1;
- }
- }
- //Create the new repo object
- const newRepo = {
- id: newID,
- alias: alias,
- repositoryName: '',
- status: false,
- lastSave: 0,
- alert: alert,
- storageSize: Number(size),
- storageUsed: 0,
- sshPublicKey: sshPublicKey,
- comment: comment,
- displayDetails: true,
- lanCommand: lanCommand,
- appendOnlyMode: appendOnlyMode,
- };
-
- ////Call the shell : createRepo.sh
- //Find the absolute path of the shells directory
- const shellsDirectory = path.join(process.cwd(), '/helpers');
- //Exec the shell
- const { stdout } = await exec(
- `${shellsDirectory}/shells/createRepo.sh "${newRepo.sshPublicKey}" ${newRepo.storageSize} ${newRepo.appendOnlyMode}`
- );
-
- newRepo.repositoryName = stdout.trim();
-
- //Create the new repoList with the new repo
- let newRepoList = [newRepo, ...repoList];
-
- //History the new repoList
- await repoHistory(newRepoList);
-
- //Stringify the newRepoList to write it into the json file.
- newRepoList = JSON.stringify(newRepoList);
-
- //Write the new json
- await fs.writeFile(
- jsonDirectory + '/repo.json',
- newRepoList,
- (err) => {
- if (err) console.log(err);
- }
- );
- res.status(200).json({ message: 'Envoi API rรฉussi' });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: error.stdout,
- });
- }
- return;
- }
- } else {
- res.status(405).json({
- status: 405,
- message: 'Method Not Allowed ',
- });
- }
-}
diff --git a/pages/api/repo/id/[slug]/delete.js b/pages/api/repo/id/[slug]/delete.js
deleted file mode 100644
index 02a1c32..0000000
--- a/pages/api/repo/id/[slug]/delete.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../../../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-import repoHistory from '../../../../../helpers/functions/repoHistory';
-const util = require('node:util');
-const exec = util.promisify(require('node:child_process').exec);
-
-export default async function handler(req, res) {
- if (req.method == 'DELETE') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- const { toDelete } = req.body;
- ////We check that we receive toDelete and it must be a bool.
- if (typeof toDelete != 'boolean' || toDelete === false) {
- //If a variable is empty.
- res.status(422).json({
- message: 'Unexpected data',
- });
- //A return to make sure we don't go any further if data are incorrect.
- return;
- }
- try {
- //console.log('API call (DELETE)');
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let repoList = await fs.readFile(
- jsonDirectory + '/repo.json',
- 'utf8'
- );
- //Parse the repoList
- repoList = JSON.parse(repoList);
-
- //Find the ID in the repoList and delete the repo.
- //NOTE : req.query.slug return a string, so parseInt to use with indexOf.
- const indexToDelete = repoList
- .map((repo) => repo.id)
- .indexOf(parseInt(req.query.slug));
-
- ////Call the shell : deleteRepo.sh
- //Find the absolute path of the shells directory
- const shellsDirectory = path.join(process.cwd(), '/helpers');
- //Exec the shell
- const { stdout, stderr } = await exec(
- `${shellsDirectory}/shells/deleteRepo.sh ${repoList[indexToDelete].repositoryName}`
- );
- if (stderr) {
- console.log('stderr:', stderr);
- res.status(500).json({
- status: 500,
- message: 'Error on delete, contact the administrator.',
- });
- return;
- }
-
- //Delete the repo in the repoList
- if (indexToDelete !== -1) {
- repoList.splice(indexToDelete, 1);
- } else {
- console.log('The index to delete does not existe (-1)');
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- return;
- }
- //History the repoList
- await repoHistory(repoList);
- //Stringify the repoList to write it into the json file.
- repoList = JSON.stringify(repoList);
- //Write the new json
- await fs.writeFile(
- jsonDirectory + '/repo.json',
- repoList,
- (err) => {
- if (err) console.log(err);
- }
- );
-
- res.status(200).json({ message: 'Envoi API rรฉussi' });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- } else {
- res.status(405).json({
- status: 405,
- message: 'Method Not Allowed ',
- });
- }
-}
diff --git a/pages/api/repo/id/[slug]/edit.js b/pages/api/repo/id/[slug]/edit.js
deleted file mode 100644
index 9726356..0000000
--- a/pages/api/repo/id/[slug]/edit.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../../../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-import repoHistory from '../../../../../helpers/functions/repoHistory';
-const util = require('node:util');
-const exec = util.promisify(require('node:child_process').exec);
-
-export default async function handler(req, res) {
- if (req.method == 'PUT') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- //The data we expect to receive
- const {
- alias,
- sshPublicKey,
- size,
- comment,
- alert,
- lanCommand,
- appendOnlyMode,
- } = req.body;
- //Only "comment" and "lanCommand" are optional in the form.
- if (
- !alias ||
- !sshPublicKey ||
- !size ||
- typeof appendOnlyMode !== 'boolean' ||
- (!alert && alert !== 0)
- ) {
- res.status(422).json({
- message: 'Unexpected data',
- });
- return;
- }
-
- try {
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- let repoList = await fs.readFile(
- jsonDirectory + '/repo.json',
- 'utf8'
- );
- //Parse the repoList
- repoList = JSON.parse(repoList);
-
- //Find the index of the repo in repoList
- //NOTE : req.query.slug return a string, so parseInt to use with indexOf.
- const repoIndex = repoList
- .map((repo) => repo.id)
- .indexOf(parseInt(req.query.slug));
-
- ////Call the shell : updateRepo.sh
- //Find the absolute path of the shells directory
- const shellsDirectory = path.join(process.cwd(), '/helpers');
- // //Exec the shell
- await exec(
- `${shellsDirectory}/shells/updateRepo.sh ${repoList[repoIndex].repositoryName} "${sshPublicKey}" ${size} ${appendOnlyMode}`
- );
-
- //Find the ID in the data and change the values transmitted by the form
- let newRepoList = repoList.map((repo) =>
- repo.id == req.query.slug
- ? {
- ...repo,
- alias: alias,
- sshPublicKey: sshPublicKey,
- storageSize: Number(size),
- comment: comment,
- alert: alert,
- lanCommand: lanCommand,
- appendOnlyMode: appendOnlyMode,
- }
- : repo
- );
- //History the new repoList
- await repoHistory(newRepoList);
- //Stringify the newRepoList to write it into the json file.
- newRepoList = JSON.stringify(newRepoList);
- //Write the new json
- await fs.writeFile(
- jsonDirectory + '/repo.json',
- newRepoList,
- (err) => {
- if (err) console.log(err);
- }
- );
-
- res.status(200).json({ message: 'Envoi API rรฉussi' });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: error.stdout,
- });
- }
- return;
- }
- } else {
- res.status(405).json({
- status: 405,
- message: 'Method Not Allowed ',
- });
- }
-}
diff --git a/pages/api/repo/id/[slug]/index.js b/pages/api/repo/id/[slug]/index.js
deleted file mode 100644
index 7462858..0000000
--- a/pages/api/repo/id/[slug]/index.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import { promises as fs } from 'fs';
-import path from 'path';
-import { authOptions } from '../../../auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'GET') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- res.status(401).json({ message: 'You must be logged in.' });
- return;
- }
-
- try {
- //console.log('API call (GET)');
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- //Read the json data file data.json
- let repoList = await fs.readFile(
- jsonDirectory + '/repo.json',
- 'utf8'
- );
- //Parse the json data file who has been read
- repoList = JSON.parse(repoList);
- //Find the ID (req.query.slug) in RepoList and put the repo in a single object (repo).
- let repo;
- for (let element in repoList) {
- if (repoList[element].id == req.query.slug) {
- repo = repoList[element];
- }
- }
- //If no repo is found --> 404.
- if (!repo) {
- res.status(404).json({
- message: 'No repository with id #' + req.query.slug,
- });
- return;
- }
- // Send the response and return the repo object --> 200
- res.status(200).json({ repo });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator',
- });
- }
- return;
- }
- }
-}
diff --git a/pages/api/repo/index.js b/pages/api/repo/index.js
deleted file mode 100644
index 4d350c6..0000000
--- a/pages/api/repo/index.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import fs from 'fs';
-import path from 'path';
-import { authOptions } from '../../../pages/api/auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default async function handler(req, res) {
- if (req.method == 'GET') {
- //Verify that the user is logged in.
- const session = await getServerSession(req, res, authOptions);
- if (!session) {
- // res.status(401).json({ message: 'You must be logged in.' });
- res.status(401).end();
- return;
- }
-
- try {
- //console.log('API call (GET)');
- //Find the absolute path of the json directory
- const jsonDirectory = path.join(process.cwd(), '/config');
- //Check if the repo.json file exists and initialize it if not.
- if (!fs.existsSync(jsonDirectory + '/repo.json')) {
- fs.writeFileSync(
- jsonDirectory + '/repo.json',
- JSON.stringify([])
- );
- }
- //Read the file repo.json
- let repoList = await fs.promises.readFile(
- jsonDirectory + '/repo.json',
- 'utf8'
- );
- //Parse the JSON
- repoList = JSON.parse(repoList);
- //Send the response
- res.status(200).json({ repoList });
- } catch (error) {
- //Log for backend
- console.log(error);
- //Log for frontend
- if (error.code == 'ENOENT') {
- res.status(500).json({
- status: 500,
- message: 'No such file or directory',
- });
- } else {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator !',
- });
- }
- return;
- }
- }
-}
diff --git a/pages/api/v1/account/email.test.ts b/pages/api/v1/account/email.test.ts
new file mode 100644
index 0000000..a4c8770
--- /dev/null
+++ b/pages/api/v1/account/email.test.ts
@@ -0,0 +1,157 @@
+import { createMocks } from 'node-mocks-http';
+import handler from '~/pages/api/v1/account/email';
+import { getServerSession } from 'next-auth/next';
+import { ConfigService } from '~/services';
+
+vi.mock('next-auth/next');
+vi.mock('~/services');
+
+describe('PUT on email API', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 401 if not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+
+ const { req, res } = createMocks({ method: 'PUT' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 405 if method is not PUT', async () => {
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 422 if email is not provided', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: {},
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(422);
+ expect(res._getJSONData()).toEqual({ message: 'Unexpected data' });
+ });
+
+ it('should return 400 if user is not found in the users list', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ { id: 1, username: 'Ada', email: 'ada@example.com', password: '', roles: [] },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { email: 'new@example.com' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ });
+
+ it('should return 400 if email already exists', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ { id: 1, username: 'Lovelace', email: 'lovelace@example.com', password: '', roles: [] },
+ { id: 2, username: 'Ada', email: 'new@example.com', password: '', roles: [] },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { email: 'new@example.com' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({ message: 'Email already exists' });
+ });
+
+ it('should update the email and return 200 on success', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ const users = [
+ { id: 1, username: 'Lovelace', email: 'lovelace@example.com', password: '', roles: [] },
+ ];
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue(users);
+ vi.mocked(ConfigService.updateUsersList).mockResolvedValue();
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { email: 'new@example.com' },
+ });
+
+ await handler(req, res);
+
+ expect(ConfigService.updateUsersList).toHaveBeenCalledWith([
+ { ...users[0], email: 'new@example.com' },
+ ]);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ message: 'Successful API send' });
+ });
+
+ it('should return 500 if there is a file system error', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'ENOENT' });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { email: 'new@example.com' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'No such file or directory',
+ });
+ });
+
+ it('should return 500 on unknown error', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'UNKNOWN_ERROR' });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { email: 'new@example.com' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'API error, contact the administrator',
+ });
+ });
+});
diff --git a/pages/api/v1/account/email.ts b/pages/api/v1/account/email.ts
new file mode 100644
index 0000000..4033122
--- /dev/null
+++ b/pages/api/v1/account/email.ts
@@ -0,0 +1,50 @@
+import { ConfigService } from '~/services';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { EmailSettingDTO, ErrorResponse } from '~/types';
+import ApiResponse from '~/helpers/functions/apiResponse';
+
+export default async function handler(
+ req: NextApiRequest & { body: EmailSettingDTO },
+ res: NextApiResponse
+) {
+ if (req.method !== 'PUT') {
+ return res.status(405);
+ }
+
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return res.status(401);
+ }
+
+ const { email } = req.body;
+
+ if (!email) {
+ return res.status(422).json({ message: 'Unexpected data' });
+ }
+
+ try {
+ const usersList = await ConfigService.getUsersList();
+ const userIndex = usersList.findIndex((user) => user.username === session.user?.name);
+
+ if (userIndex === -1) {
+ return res.status(400).json({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ }
+
+ if (usersList.some((user) => user.email === email)) {
+ return res.status(400).json({ message: 'Email already exists' });
+ }
+
+ const updatedUsersList = usersList.map((user, index) =>
+ index === userIndex ? { ...user, email } : user
+ );
+
+ await ConfigService.updateUsersList(updatedUsersList);
+ return res.status(200).json({ message: 'Successful API send' });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+}
diff --git a/pages/api/v1/account/password.test.ts b/pages/api/v1/account/password.test.ts
new file mode 100644
index 0000000..0ec141a
--- /dev/null
+++ b/pages/api/v1/account/password.test.ts
@@ -0,0 +1,167 @@
+import { createMocks } from 'node-mocks-http';
+import handler from '~/pages/api/v1/account/password';
+import { getServerSession } from 'next-auth/next';
+import { ConfigService, AuthService } from '~/services';
+
+vi.mock('next-auth/next');
+vi.mock('~/services');
+
+describe('PUT /api/account/updatePassword', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 401 if not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+
+ const { req, res } = createMocks({ method: 'PUT' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 405 if method is not PUT', async () => {
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 422 if oldPassword or newPassword are missing or not strings', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { oldPassword: 1234, newPassword: true },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(422);
+ expect(res._getJSONData()).toEqual({ message: 'Unexpected data' });
+ });
+
+ it('should return 400 if user is not found', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ { id: 1, username: 'Ada', password: 'hashedpass', roles: [], email: 'ada@example.com' },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { oldPassword: 'test', newPassword: 'newpass' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ });
+
+ it('should return 400 if old password is incorrect', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ { id: 1, username: 'Lovelace', password: 'hashedpass', roles: [], email: 'love@example.com' },
+ ]);
+
+ vi.mocked(AuthService.verifyPassword).mockResolvedValue(false);
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { oldPassword: 'wrongpass', newPassword: 'newpass' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({ message: 'Old password is incorrect.' });
+ });
+
+ it('should update password and return 200 on success', async () => {
+ const oldUser = {
+ id: 1,
+ username: 'Lovelace',
+ password: 'hashedpass',
+ roles: [],
+ email: 'love@example.com',
+ };
+
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([oldUser]);
+
+ vi.mocked(AuthService.verifyPassword).mockResolvedValue(true);
+ vi.mocked(AuthService.hashPassword).mockResolvedValue('newHashedPassword');
+ vi.mocked(ConfigService.updateUsersList).mockResolvedValue();
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { oldPassword: 'oldpass', newPassword: 'newpass' },
+ });
+
+ await handler(req, res);
+
+ expect(AuthService.verifyPassword).toHaveBeenCalledWith('oldpass', 'hashedpass');
+ expect(AuthService.hashPassword).toHaveBeenCalledWith('newpass');
+ expect(ConfigService.updateUsersList).toHaveBeenCalledWith([
+ { ...oldUser, password: 'newHashedPassword' },
+ ]);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ message: 'Successful API send' });
+ });
+
+ it('should return 500 if there is a file system error (ENOENT)', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'ENOENT' });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { oldPassword: 'test', newPassword: 'new' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'No such file or directory',
+ });
+ });
+
+ it('should return 500 on unknown error', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'SOMETHING_ELSE' });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { oldPassword: 'test', newPassword: 'new' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'API error, contact the administrator',
+ });
+ });
+});
diff --git a/pages/api/v1/account/password.ts b/pages/api/v1/account/password.ts
new file mode 100644
index 0000000..015e3c6
--- /dev/null
+++ b/pages/api/v1/account/password.ts
@@ -0,0 +1,54 @@
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { ConfigService, AuthService } from '~/services';
+import { getServerSession } from 'next-auth/next';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { ErrorResponse, PasswordSettingDTO } from '~/types';
+import ApiResponse from '~/helpers/functions/apiResponse';
+
+export default async function handler(
+ req: NextApiRequest & { body: PasswordSettingDTO },
+ res: NextApiResponse
+) {
+ if (req.method !== 'PUT') {
+ return res.status(405);
+ }
+
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return res.status(401);
+ }
+
+ const { oldPassword, newPassword } = req.body;
+
+ if (typeof oldPassword !== 'string' || typeof newPassword !== 'string') {
+ return res.status(422).json({ message: 'Unexpected data' });
+ }
+
+ try {
+ const usersList = await ConfigService.getUsersList();
+ const userIndex = usersList.findIndex((user) => user.username === session.user?.name);
+ const user = usersList[userIndex];
+
+ if (userIndex === -1 || !user) {
+ return res.status(400).json({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ }
+
+ const isValidPassword = await AuthService.verifyPassword(oldPassword, user.password);
+ if (!isValidPassword) {
+ return res.status(400).json({ message: 'Old password is incorrect.' });
+ }
+
+ const newPasswordHash = await AuthService.hashPassword(newPassword);
+ const updatedUsersList = usersList.map((user, index) =>
+ index === userIndex ? { ...user, password: newPasswordHash } : user
+ );
+
+ await ConfigService.updateUsersList(updatedUsersList);
+
+ return res.status(200).json({ message: 'Successful API send' });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+}
diff --git a/pages/api/v1/account/username.test.ts b/pages/api/v1/account/username.test.ts
new file mode 100644
index 0000000..8bab705
--- /dev/null
+++ b/pages/api/v1/account/username.test.ts
@@ -0,0 +1,171 @@
+import { createMocks } from 'node-mocks-http';
+import handler from '~/pages/api/v1/account/username';
+import { getServerSession } from 'next-auth/next';
+import { ConfigService } from '~/services';
+
+vi.mock('next-auth/next');
+vi.mock('~/services');
+
+describe('PUT /api/account/updateUsername', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.resetModules();
+ vi.resetAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 401 if not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+
+ const { req, res } = createMocks({ method: 'PUT' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 405 if method is not PUT', async () => {
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 422 if username is not a string', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'Lovelace' } });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { username: 12345 },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(422);
+ expect(res._getJSONData()).toEqual({ message: 'Unexpected data' });
+ });
+
+ it('should return 422 if username format is invalid', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'Lovelace' } });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { username: '' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(422);
+ expect(res._getJSONData()).toEqual({
+ message: 'Only a-z characters are allowed (1 to 40 char.)',
+ });
+ });
+
+ it('should return 400 if user is not found', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'Lovelace' } });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ { username: 'Ada', email: 'ada@example.com', password: 'xxx', id: 1, roles: ['user'] },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { username: 'newname' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ });
+
+ it('should return 400 if new username already exists', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'Lovelace' } });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ { username: 'Lovelace', email: 'love@example.com', password: 'xxx', id: 1, roles: ['user'] },
+ {
+ username: 'newname',
+ email: 'someone@example.com',
+ password: 'xxx',
+ id: 2,
+ roles: ['user'],
+ },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { username: 'newname' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({ message: 'Username already exists' });
+ });
+
+ it('should return 200 and update the username', async () => {
+ const originalUser = {
+ username: 'Lovelace',
+ email: 'love@example.com',
+ password: 'xxx',
+ id: 1,
+ roles: ['user'],
+ };
+
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'Lovelace' } });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([originalUser]);
+ vi.mocked(ConfigService.updateUsersList).mockResolvedValue();
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { username: 'newusername' },
+ });
+
+ await handler(req, res);
+
+ expect(ConfigService.updateUsersList).toHaveBeenCalledWith([
+ { ...originalUser, username: 'newusername' },
+ ]);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ message: 'Successful API send' });
+ });
+
+ it('should return 500 if file not found (ENOENT)', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'Lovelace' } });
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'ENOENT' });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { username: 'newname' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'No such file or directory',
+ });
+ });
+
+ it('should return 500 on unknown error', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'Lovelace' } });
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'SOMETHING_ELSE' });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { username: 'newname' },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'API error, contact the administrator',
+ });
+ });
+});
diff --git a/pages/api/v1/account/username.ts b/pages/api/v1/account/username.ts
new file mode 100644
index 0000000..354b6cd
--- /dev/null
+++ b/pages/api/v1/account/username.ts
@@ -0,0 +1,58 @@
+import { ConfigService } from '~/services';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { ErrorResponse, UsernameSettingDTO } from '~/types';
+import ApiResponse from '~/helpers/functions/apiResponse';
+
+export default async function handler(
+ req: NextApiRequest & { body: UsernameSettingDTO },
+ res: NextApiResponse
+) {
+ if (req.method !== 'PUT') {
+ return res.status(405);
+ }
+
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return res.status(401);
+ }
+
+ //The data we expect to receive
+ const { username } = req.body;
+
+ if (typeof username !== 'string') {
+ return res.status(422).json({ message: 'Unexpected data' });
+ }
+ const usernameRegex = new RegExp(/^[a-z]{1,40}$/);
+ if (!usernameRegex.test(username)) {
+ res.status(422).json({
+ message: 'Only a-z characters are allowed (1 to 40 char.)',
+ });
+ return;
+ }
+
+ try {
+ const usersList = await ConfigService.getUsersList();
+ const userIndex = usersList.findIndex((user) => user.username === session.user?.name);
+
+ if (userIndex === -1) {
+ return res.status(400).json({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ }
+
+ if (usersList.some((user) => user.username === username)) {
+ return res.status(400).json({ message: 'Username already exists' });
+ }
+
+ const updatedUsersList = usersList.map((user, index) =>
+ index === userIndex ? { ...user, username } : user
+ );
+ await ConfigService.updateUsersList(updatedUsersList);
+
+ return res.status(200).json({ message: 'Successful API send' });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+}
diff --git a/pages/api/v1/account/wizard-env.test.ts b/pages/api/v1/account/wizard-env.test.ts
new file mode 100644
index 0000000..de70014
--- /dev/null
+++ b/pages/api/v1/account/wizard-env.test.ts
@@ -0,0 +1,48 @@
+import { createMocks } from 'node-mocks-http';
+import handler from '~/pages/api/v1/account/wizard-env';
+import { getServerSession } from 'next-auth/next';
+
+vi.mock('next-auth/next');
+
+describe('Get Wizard Env API', () => {
+ it('should return 405 if the method is not GET', async () => {
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if the user is not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 200 with wizardEnv if the user is authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testuser' } });
+
+ process.env.UNIX_USER = 'borgwarehouse';
+ process.env.FQDN = 'localhost';
+ process.env.SSH_SERVER_PORT = '22';
+ process.env.HIDE_SSH_PORT = 'false';
+ process.env.DISABLE_INTEGRATIONS = 'false';
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ UNIX_USER: 'borgwarehouse',
+ FQDN: 'localhost',
+ SSH_SERVER_PORT: '22',
+ FQDN_LAN: '',
+ SSH_SERVER_PORT_LAN: '',
+ SSH_SERVER_FINGERPRINT_RSA: '',
+ SSH_SERVER_FINGERPRINT_ED25519: '',
+ SSH_SERVER_FINGERPRINT_ECDSA: '',
+ HIDE_SSH_PORT: 'false',
+ DISABLE_INTEGRATIONS: 'false',
+ DISABLE_DELETE_REPO: 'false',
+ });
+ });
+});
diff --git a/pages/api/v1/account/wizard-env.ts b/pages/api/v1/account/wizard-env.ts
new file mode 100644
index 0000000..548aebf
--- /dev/null
+++ b/pages/api/v1/account/wizard-env.ts
@@ -0,0 +1,43 @@
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { ErrorResponse, WizardEnvEnum, WizardEnvType } from '~/types';
+import ApiResponse from '~/helpers/functions/apiResponse';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ if (req.method !== 'GET') {
+ return res.status(405);
+ }
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return res.status(401);
+ }
+
+ try {
+ function getEnvVariable(envName: WizardEnvEnum, defaultValue = '') {
+ return process.env[envName] || defaultValue;
+ }
+
+ const wizardEnv: WizardEnvType = {
+ UNIX_USER: getEnvVariable(WizardEnvEnum.UNIX_USER, 'borgwarehouse'),
+ FQDN: getEnvVariable(WizardEnvEnum.FQDN, 'localhost'),
+ SSH_SERVER_PORT: getEnvVariable(WizardEnvEnum.SSH_SERVER_PORT, '22'),
+ FQDN_LAN: getEnvVariable(WizardEnvEnum.FQDN_LAN),
+ SSH_SERVER_PORT_LAN: getEnvVariable(WizardEnvEnum.SSH_SERVER_PORT_LAN),
+ SSH_SERVER_FINGERPRINT_RSA: getEnvVariable(WizardEnvEnum.SSH_SERVER_FINGERPRINT_RSA),
+ SSH_SERVER_FINGERPRINT_ED25519: getEnvVariable(WizardEnvEnum.SSH_SERVER_FINGERPRINT_ED25519),
+ SSH_SERVER_FINGERPRINT_ECDSA: getEnvVariable(WizardEnvEnum.SSH_SERVER_FINGERPRINT_ECDSA),
+ HIDE_SSH_PORT: getEnvVariable(WizardEnvEnum.HIDE_SSH_PORT, 'false'),
+ DISABLE_INTEGRATIONS: getEnvVariable(WizardEnvEnum.DISABLE_INTEGRATIONS, 'false'),
+ DISABLE_DELETE_REPO: getEnvVariable(WizardEnvEnum.DISABLE_DELETE_REPO, 'false'),
+ };
+
+ res.status(200).json(wizardEnv);
+ return;
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+}
diff --git a/pages/api/v1/cron/status.test.ts b/pages/api/v1/cron/status.test.ts
new file mode 100644
index 0000000..a89d3f1
--- /dev/null
+++ b/pages/api/v1/cron/status.test.ts
@@ -0,0 +1,520 @@
+import { createMocks } from 'node-mocks-http';
+import handler from '~/pages/api/v1/cron/status';
+import { ConfigService, NotifService, ShellService } from '~/services';
+import { AppriseModeEnum } from '~/types/domain/config.types';
+import * as childProcess from 'node:child_process';
+
+vi.mock('~/services', () => ({
+ NotifService: {
+ nodemailerSMTP: vi.fn(() => ({
+ sendMail: vi.fn().mockResolvedValue({ messageId: 'fake-message-id' }),
+ })),
+ },
+ ConfigService: {
+ getRepoList: vi.fn(),
+ updateRepoList: vi.fn(),
+ getUsersList: vi.fn(),
+ },
+ ShellService: {
+ getLastSaveList: vi.fn(),
+ },
+}));
+vi.mock('~/helpers/templates/emailAlertStatus', () => ({
+ default: vi.fn(() => ({
+ subject: 'Alert',
+ text: 'Alert text',
+ })),
+}));
+
+vi.mock('node:child_process', () => ({
+ exec: vi.fn(
+ (callback: (err: Error | null, result: { stdout: string; stderr: string }) => void) => {
+ callback(null, { stdout: 'mocked output', stderr: '' });
+ }
+ ),
+}));
+
+vi.mock('date-fns', () => {
+ return {
+ getUnixTime: vi.fn(() => 1741535661),
+ };
+});
+
+describe('Cronjob API Handler', () => {
+ beforeEach(() => {
+ process.env.CRONJOB_KEY = 'test-key';
+ vi.clearAllMocks();
+ vi.resetModules();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 401 if no authorization header', async () => {
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 401 if method is not POST', async () => {
+ const { req, res } = createMocks({
+ method: 'GET',
+ headers: { authorization: 'Bearer test-key' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 401 if wrong authorization key', async () => {
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer wrong-key' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 200 with message if no repository to check (empty repoList)', async () => {
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
+ vi.mocked(ShellService.getLastSaveList).mockResolvedValue([
+ { repositoryName: 'repo1', lastSave: 123 },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ status: 200,
+ message: 'Status cron executed. No repository to check.',
+ });
+ });
+
+ it('should return 200 with message if no repository to check (empty lastSaveList)', async () => {
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ repositoryName: 'repo1',
+ alert: 100,
+ alias: 'Repo1',
+ id: 1,
+ status: true,
+ lastSave: 0,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ vi.mocked(ShellService.getLastSaveList).mockResolvedValue([]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ status: 200,
+ message: 'Status cron executed. No repository to check.',
+ });
+ });
+
+ it('should execute successfully without alerts if all repositories are OK', async () => {
+ const currentTime = Math.floor(Date.now() / 1000);
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ repositoryName: 'repo1',
+ alert: 1000,
+ alias: 'Repo1',
+ status: true,
+ id: 1,
+ lastSave: 0,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ vi.mocked(ShellService.getLastSaveList).mockResolvedValue([
+ { repositoryName: 'repo1', lastSave: currentTime },
+ ]);
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue(undefined);
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ status: 200,
+ message: 'Status cron executed successfully',
+ });
+ expect(ConfigService.updateRepoList).toHaveBeenCalled();
+ });
+
+ it('should return 500 if an error occurs', async () => {
+ vi.mocked(ConfigService.getRepoList).mockRejectedValue(
+ new Error('Repo list could not be fetched')
+ );
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'Repo list could not be fetched',
+ });
+ });
+
+ it('should not send email alert if emailAlert is false', async () => {
+ const currentTime = Math.floor(Date.now() / 1000);
+
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ repositoryName: 'repo1',
+ alert: 100,
+ alias: 'Repo1',
+ id: 1,
+ status: true,
+ lastSave: 0,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ vi.mocked(ShellService.getLastSaveList).mockResolvedValue([
+ { repositoryName: 'repo1', lastSave: currentTime - 200 },
+ ]);
+ // User has disabled email alert but enabled Apprise alert
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ password: 'hashed-password',
+ roles: ['user'],
+ emailAlert: false,
+ appriseAlert: true,
+ appriseServices: ['http://example.com'],
+ appriseMode: AppriseModeEnum.PACKAGE,
+ appriseStatelessURL: 'http://example.com',
+ email: 'test@example.com',
+ username: 'testuser',
+ },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+ await handler(req, res);
+
+ expect(NotifService.nodemailerSMTP).not.toHaveBeenCalled();
+ });
+
+ it('should not send apprise alert if appriseAlert is false', async () => {
+ const currentTime = Math.floor(Date.now() / 1000);
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ repositoryName: 'repo1',
+ alert: 100,
+ alias: 'Repo1',
+ id: 1,
+ status: true,
+ lastSave: 0,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ vi.mocked(ShellService.getLastSaveList).mockResolvedValue([
+ { repositoryName: 'repo1', lastSave: currentTime - 200 },
+ ]);
+ // User has disabled Apprise alert but enabled email alert
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ password: 'hashed-password',
+ roles: ['user'],
+ emailAlert: true,
+ appriseAlert: false,
+ appriseServices: ['http://example.com'],
+ appriseMode: AppriseModeEnum.PACKAGE,
+ appriseStatelessURL: 'http://example.com',
+ email: 'test@example.com',
+ username: 'testuser',
+ },
+ ]);
+
+ // Spy on exec to check if it is called
+ const execSpy = vi.spyOn(childProcess, 'exec');
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+ await handler(req, res);
+
+ expect(execSpy).not.toHaveBeenCalled();
+ execSpy.mockRestore();
+ });
+
+ it('should not send alert if alert is disabled on repo (repo.alert === 0)', async () => {
+ const currentTime = Math.floor(Date.now() / 1000);
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ repositoryName: 'repo1',
+ alert: 0,
+ alias: 'Repo1',
+ id: 1,
+ status: false,
+ lastSave: 0,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ vi.mocked(ShellService.getLastSaveList).mockResolvedValue([
+ { repositoryName: 'repo1', lastSave: currentTime - 1000 },
+ ]);
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ password: 'hashed-password',
+ roles: ['user'],
+ emailAlert: true,
+ appriseAlert: true,
+ appriseServices: ['http://example.com'],
+ appriseMode: AppriseModeEnum.PACKAGE,
+ appriseStatelessURL: 'http://example.com',
+ email: 'test@example.com',
+ username: 'testuser',
+ },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+ await handler(req, res);
+
+ expect(NotifService.nodemailerSMTP).not.toHaveBeenCalled();
+
+ const childProcess = await import('node:child_process');
+ expect(childProcess.exec).not.toHaveBeenCalled();
+ });
+
+ it('should not update lastStatusAlertSend or add to repoListToSendAlert if repo status is OK', async () => {
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ repositoryName: 'repo1',
+ status: true,
+ alert: 100,
+ id: 1,
+ alias: 'Repo1',
+ lastSave: 0,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ lastStatusAlertSend: 1000,
+ },
+ ]);
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue(undefined);
+ vi.mocked(ShellService.getLastSaveList).mockResolvedValue([
+ { repositoryName: 'repo1', lastSave: Math.floor(Date.now() / 1000) },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+
+ await handler(req, res);
+
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith([
+ {
+ repositoryName: 'repo1',
+ status: true,
+ alert: 100,
+ id: 1,
+ alias: 'Repo1',
+ lastSave: expect.any(Number),
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ lastStatusAlertSend: 1000,
+ },
+ ]);
+ expect(res._getStatusCode()).toBe(200);
+ });
+
+ it('should update lastStatusAlertSend if repo is down and alert is enabled', async () => {
+ const currentTime = 1741535661;
+
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ repositoryName: 'repo1',
+ alias: 'Repo1',
+ status: false,
+ alert: 100,
+ id: 1,
+ lastSave: 0,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ vi.mocked(ShellService.getLastSaveList).mockResolvedValue([
+ { repositoryName: 'repo1', lastSave: currentTime - 200 },
+ ]);
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ password: 'hashed-password',
+ roles: ['user'],
+ emailAlert: true,
+ email: 'test@example.com',
+ username: 'TestUser',
+ },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+
+ await handler(req, res);
+
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith([
+ {
+ repositoryName: 'repo1',
+ alias: 'Repo1',
+ status: false,
+ alert: 100,
+ id: 1,
+ lastSave: currentTime - 200,
+ lastStatusAlertSend: currentTime,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ expect(res._getStatusCode()).toBe(200);
+ });
+
+ it('should not update lastStatusAlertSend or send alerts if alert is disabled', async () => {
+ const currentTime = 1741535661;
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ repositoryName: 'repo1',
+ alias: 'Repo1',
+ status: false,
+ alert: 0,
+ lastStatusAlertSend: undefined,
+ id: 1,
+ lastSave: 0,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ vi.mocked(ShellService.getLastSaveList).mockResolvedValue([
+ { repositoryName: 'repo1', lastSave: currentTime - 200 },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+
+ await handler(req, res);
+
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith([
+ {
+ repositoryName: 'repo1',
+ alias: 'Repo1',
+ status: false,
+ alert: 0,
+ lastStatusAlertSend: undefined,
+ id: 1,
+ lastSave: currentTime - 200,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ expect(NotifService.nodemailerSMTP).not.toHaveBeenCalled();
+ expect(res._getStatusCode()).toBe(200);
+ });
+
+ it('should update lastStatusAlertSend only if the last alert was sent more than 90000 seconds ago', async () => {
+ const currentTime = 1741535661;
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ repositoryName: 'repo1',
+ alias: 'Repo1',
+ status: false,
+ alert: 100,
+ lastStatusAlertSend: currentTime - 80000,
+ id: 1,
+ lastSave: 0,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ vi.mocked(ShellService.getLastSaveList).mockResolvedValue([
+ { repositoryName: 'repo1', lastSave: currentTime - 200 },
+ ]);
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ password: 'hashed-password',
+ roles: ['user'],
+ emailAlert: true,
+ email: 'test@example.com',
+ username: 'TestUser',
+ },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer test-key' },
+ });
+
+ await handler(req, res);
+
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith([
+ {
+ repositoryName: 'repo1',
+ alias: 'Repo1',
+ status: false,
+ alert: 100,
+ lastStatusAlertSend: expect.any(Number),
+ id: 1,
+ lastSave: currentTime - 200,
+ storageSize: 0,
+ storageUsed: 0,
+ sshPublicKey: '',
+ comment: '',
+ },
+ ]);
+ expect(res._getStatusCode()).toBe(200);
+ });
+});
diff --git a/pages/api/v1/cron/status.ts b/pages/api/v1/cron/status.ts
new file mode 100644
index 0000000..5ca9c8e
--- /dev/null
+++ b/pages/api/v1/cron/status.ts
@@ -0,0 +1,108 @@
+import { getUnixTime } from 'date-fns';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { exec as execCallback } from 'node:child_process';
+import { promisify } from 'util';
+import ApiResponse from '~/helpers/functions/apiResponse';
+import { ConfigService, NotifService, ShellService } from '~/services';
+import emailAlertStatus from '~/helpers/templates/emailAlertStatus';
+import { BorgWarehouseApiResponse } from '~/types';
+
+const exec = promisify(execCallback);
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ if (!req.headers.authorization) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ const CRONJOB_KEY = process.env.CRONJOB_KEY;
+ const ACTION_KEY = req.headers.authorization.split(' ')[1];
+
+ if (req.method !== 'POST' || ACTION_KEY !== CRONJOB_KEY) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ try {
+ const repoList = await ConfigService.getRepoList();
+ const lastSaveList = await ShellService.getLastSaveList();
+ if (repoList.length === 0 || lastSaveList.length === 0) {
+ return ApiResponse.success(res, 'Status cron executed. No repository to check.');
+ }
+ const date = getUnixTime(new Date());
+
+ // Update the status and the last timestamp backup of each repository
+ const updatedRepoList = repoList.map((repo) => {
+ const repoFiltered = lastSaveList.find((x) => x.repositoryName === repo.repositoryName);
+ if (!repoFiltered) return repo;
+ const lastSaveTimestamp = repoFiltered.lastSave;
+ return {
+ ...repo,
+ lastSave: lastSaveTimestamp,
+ status: date - lastSaveTimestamp <= (repo?.alert ?? 0),
+ };
+ });
+
+ const repoAliasListToSendAlert: string[] = [];
+ updatedRepoList.forEach((repo) => {
+ if (
+ !repo.status &&
+ repo.alert !== 0 &&
+ (!repo.lastStatusAlertSend || date - repo.lastStatusAlertSend > 90000)
+ ) {
+ repo.lastStatusAlertSend = date;
+ repoAliasListToSendAlert.push(repo.alias);
+ }
+ });
+
+ if (repoAliasListToSendAlert.length > 0) {
+ const usersList = await ConfigService.getUsersList();
+
+ // Send Email Alert
+ if (usersList[0].emailAlert) {
+ const transporter = NotifService.nodemailerSMTP();
+ const mailData = emailAlertStatus(
+ usersList[0].email,
+ usersList[0].username,
+ repoAliasListToSendAlert
+ );
+ transporter.sendMail(mailData, (err) => {
+ if (err) console.log(err);
+ });
+ }
+
+ // Send Apprise Alert
+ if (usersList[0].appriseAlert) {
+ const appriseServicesURLs = usersList[0].appriseServices?.join(' ');
+ const message = `๐ด Some repositories on BorgWarehouse need attention !\nList of down repositories :\n ${repoAliasListToSendAlert}`;
+
+ try {
+ if (usersList[0].appriseMode === 'package') {
+ await exec(`apprise -v -b '${message}' ${appriseServicesURLs}`);
+ } else if (usersList[0].appriseMode === 'stateless') {
+ await fetch(`${usersList[0].appriseStatelessURL}/notify`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ urls: appriseServicesURLs, body: message }),
+ });
+ } else {
+ console.warn('No Apprise Mode selected or supported.');
+ }
+ } catch (notifErr) {
+ console.error('Apprise notification failed:', notifErr);
+ }
+ }
+ }
+
+ await ConfigService.updateRepoList(updatedRepoList);
+ return ApiResponse.success(res, 'Status cron executed successfully');
+ } catch (error) {
+ console.log(error);
+ if (error instanceof Error && error.message === 'The check status service is already running') {
+ return ApiResponse.conflict(res, error.message);
+ } else {
+ return ApiResponse.serverError(res, error);
+ }
+ }
+}
diff --git a/pages/api/v1/cron/storage.test.ts b/pages/api/v1/cron/storage.test.ts
new file mode 100644
index 0000000..bf2c429
--- /dev/null
+++ b/pages/api/v1/cron/storage.test.ts
@@ -0,0 +1,128 @@
+import handler from '~/pages/api/v1/cron/storage';
+import { createMocks } from 'node-mocks-http';
+import { ConfigService, ShellService } from '~/services';
+import { Repository } from '~/types';
+
+vi.mock('~/services');
+
+describe('GET /api/cronjob/getStorageUsed', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ const CRONJOB_KEY = 'test-cronjob-key';
+ process.env.CRONJOB_KEY = CRONJOB_KEY;
+
+ it('should return unauthorized if no authorization header is provided', async () => {
+ const { req, res } = createMocks({
+ method: 'POST',
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return unauthorized if the authorization key is invalid', async () => {
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: {
+ authorization: 'Bearer invalid-key',
+ },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return success if no repositories are found', async () => {
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: {
+ authorization: `Bearer ${CRONJOB_KEY}`,
+ },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getData()).toContain('No repository to check');
+ });
+
+ it('should update repositories with storage used and return success', async () => {
+ const mockRepoList = [
+ { repositoryName: 'repo1', storageUsed: 0 },
+ { repositoryName: 'repo2', storageUsed: 0 },
+ ] as Repository[];
+ const mockStorageUsed = [
+ { name: 'repo1', size: 100 },
+ { name: 'repo2', size: 200 },
+ ];
+
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue(mockRepoList);
+ vi.mocked(ShellService.getStorageUsed).mockResolvedValue(mockStorageUsed);
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue(undefined);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: {
+ authorization: `Bearer ${CRONJOB_KEY}`,
+ },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getData()).toContain('Storage cron executed successfully');
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith([
+ { repositoryName: 'repo1', storageUsed: 100 },
+ { repositoryName: 'repo2', storageUsed: 200 },
+ ]);
+ });
+
+ it('should return server error if an exception occurs', async () => {
+ vi.mocked(ConfigService.getRepoList).mockRejectedValue(new Error('Test error'));
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: {
+ authorization: `Bearer ${CRONJOB_KEY}`,
+ },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ });
+
+ it('should not touch to a repository if it is not found in the storage used list', async () => {
+ const mockRepoList = [
+ { repositoryName: 'repo1', storageUsed: 0 },
+ { repositoryName: 'repo2', storageUsed: 0 },
+ ] as Repository[];
+ const mockStorageUsed = [{ name: 'repo1', size: 100 }];
+
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue(mockRepoList);
+ vi.mocked(ShellService.getStorageUsed).mockResolvedValue(mockStorageUsed);
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue(undefined);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: {
+ authorization: `Bearer ${CRONJOB_KEY}`,
+ },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith([
+ { repositoryName: 'repo1', storageUsed: 100 },
+ { repositoryName: 'repo2', storageUsed: 0 },
+ ]);
+ });
+});
diff --git a/pages/api/v1/cron/storage.ts b/pages/api/v1/cron/storage.ts
new file mode 100644
index 0000000..2740954
--- /dev/null
+++ b/pages/api/v1/cron/storage.ts
@@ -0,0 +1,50 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import { ConfigService, ShellService } from '~/services';
+import ApiResponse from '~/helpers/functions/apiResponse';
+import { BorgWarehouseApiResponse } from '~/types';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ if (!req.headers.authorization) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ const CRONJOB_KEY = process.env.CRONJOB_KEY;
+ const ACTION_KEY = req.headers.authorization.split(' ')[1];
+
+ if (req.method !== 'POST' || ACTION_KEY !== CRONJOB_KEY) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ try {
+ //Check the repoList
+ const repoList = await ConfigService.getRepoList();
+ if (repoList.length === 0) {
+ return ApiResponse.success(res, 'Storage cron executed. No repository to check.');
+ }
+
+ const storageUsed = await ShellService.getStorageUsed();
+
+ //Update the storageUsed value of each repository
+ const updatedRepoList = repoList.map((repo) => {
+ const repoFiltered = storageUsed.find((x) => x.name === repo.repositoryName);
+ if (!repoFiltered) return repo;
+ return {
+ ...repo,
+ storageUsed: repoFiltered.size,
+ };
+ });
+
+ await ConfigService.updateRepoList(updatedRepoList);
+ return ApiResponse.success(res, 'Storage cron executed successfully');
+ } catch (error) {
+ console.log(error);
+ if (error instanceof Error && error.message === 'The storage used service is already running') {
+ return ApiResponse.conflict(res, error.message);
+ } else {
+ return ApiResponse.serverError(res, error);
+ }
+ }
+}
diff --git a/pages/api/v1/integration/token-manager.test.ts b/pages/api/v1/integration/token-manager.test.ts
new file mode 100644
index 0000000..c7d8407
--- /dev/null
+++ b/pages/api/v1/integration/token-manager.test.ts
@@ -0,0 +1,205 @@
+import handler from '~/pages/api/v1/integration/token-manager';
+import { createMocks } from 'node-mocks-http';
+import { getServerSession } from 'next-auth/next';
+import { ConfigService } from '~/services';
+import ApiResponse from '~/helpers/functions/apiResponse';
+
+vi.mock('next-auth/next', () => ({
+ __esModule: true,
+ getServerSession: vi.fn(),
+}));
+
+vi.mock('~/services');
+
+vi.mock('~/helpers/functions/apiResponse');
+
+describe('Token Manager API', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return unauthorized if session is not found', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ body: {
+ name: 'testToken',
+ permissions: { create: true, read: true, update: true, delete: true },
+ },
+ });
+
+ await handler(req, res);
+
+ expect(ApiResponse.unauthorized).toHaveBeenCalledWith(res);
+ });
+
+ it('should create a new token if valid data is provided', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testUser' } });
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testUser',
+ password: 'hashedPassword',
+ email: 'testUser@example.com',
+ roles: ['user'],
+ tokens: [],
+ },
+ ]);
+ vi.mocked(ConfigService.updateUsersList).mockResolvedValue();
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ body: {
+ name: 'testToken',
+ permissions: { create: true, read: true, update: true, delete: true },
+ },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ const responseData = JSON.parse(res._getData());
+ expect(responseData).toHaveProperty('token');
+ expect(ConfigService.updateUsersList).toHaveBeenCalled();
+ });
+
+ it('should return bad request if token name already exists', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testUser' } });
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testUser',
+ password: 'hashedPassword',
+ email: 'testUser@example.com',
+ roles: ['user'],
+ tokens: [
+ {
+ token: 'sampleToken123',
+ name: 'testToken',
+ permissions: { create: true, read: true, update: true, delete: true },
+ creation: 123,
+ },
+ ],
+ },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ body: {
+ name: 'testToken',
+ permissions: { create: true, read: true, update: true, delete: true },
+ },
+ });
+
+ await handler(req, res);
+
+ expect(ApiResponse.badRequest).toHaveBeenCalledWith(res, 'Token name already exists');
+ });
+
+ it('should return token list for GET request', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testUser' } });
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testUser',
+ password: 'hashedPassword',
+ email: 'testUser@example.com',
+ roles: ['user'],
+ tokens: [
+ {
+ token: 'sampleToken1',
+ name: 'token1',
+ permissions: { create: false, read: false, update: false, delete: false },
+ creation: 123,
+ },
+ {
+ token: 'sampleToken2',
+ name: 'token2',
+ permissions: { create: false, read: false, update: false, delete: false },
+ creation: 456,
+ },
+ ],
+ },
+ ]);
+
+ const { req, res } = createMocks({ method: 'GET' });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ const responseData = JSON.parse(res._getData());
+ expect(responseData).toEqual([
+ {
+ name: 'token1',
+ permissions: { create: false, read: false, update: false, delete: false },
+ creation: 123,
+ },
+ {
+ name: 'token2',
+ permissions: { create: false, read: false, update: false, delete: false },
+ creation: 456,
+ },
+ ]);
+ });
+
+ it('should delete a token for DELETE request', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testUser' } });
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testUser',
+ password: 'hashedPassword',
+ email: 'testUser@example.com',
+ roles: ['user'],
+ tokens: [
+ {
+ token: 'sampleToken1',
+ name: 'token1',
+ permissions: { create: false, read: false, update: false, delete: false },
+ creation: 123,
+ },
+ {
+ token: 'sampleToken2',
+ name: 'token2',
+ permissions: { create: false, read: false, update: false, delete: false },
+ creation: 456,
+ },
+ ],
+ },
+ ]);
+ vi.mocked(ConfigService.updateUsersList).mockResolvedValue();
+
+ const { req, res } = createMocks({
+ method: 'DELETE',
+ body: { name: 'token1' },
+ });
+
+ await handler(req, res);
+
+ expect(ApiResponse.success).toHaveBeenCalledWith(res, 'Token deleted');
+ expect(ConfigService.updateUsersList).toHaveBeenCalled();
+ });
+
+ it('should return bad request if token name is missing in DELETE request', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testUser' } });
+
+ const { req, res } = createMocks({
+ method: 'DELETE',
+ body: {},
+ });
+
+ await handler(req, res);
+
+ expect(ApiResponse.badRequest).toHaveBeenCalledWith(res, 'Missing token name');
+ });
+
+ it('should return method not allowed for unsupported HTTP methods', async () => {
+ const { req, res } = createMocks({ method: 'PUT' });
+
+ await handler(req, res);
+
+ expect(ApiResponse.methodNotAllowed).toHaveBeenCalledWith(res);
+ });
+});
diff --git a/pages/api/v1/integration/token-manager.ts b/pages/api/v1/integration/token-manager.ts
new file mode 100644
index 0000000..205c15b
--- /dev/null
+++ b/pages/api/v1/integration/token-manager.ts
@@ -0,0 +1,141 @@
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { NextApiRequest, NextApiResponse } from 'next';
+import ApiResponse from '~/helpers/functions/apiResponse';
+import { ConfigService } from '~/services';
+import { getUnixTime } from 'date-fns';
+import { v4 as uuidv4 } from 'uuid';
+import { BorgWarehouseApiResponse, IntegrationTokenType, TokenPermissionsType } from '~/types';
+
+export default async function handler(
+ req: NextApiRequest & { body: Partial },
+ res: NextApiResponse<
+ BorgWarehouseApiResponse | { token: string } | Omit[]
+ >
+) {
+ // Auth
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ if (req.method == 'POST') {
+ try {
+ validateRequestBody(req);
+ } catch (error) {
+ if (error instanceof Error) {
+ return ApiResponse.badRequest(res, error.message);
+ }
+ return ApiResponse.badRequest(res, 'Invalid request data');
+ }
+
+ try {
+ const { name, permissions } = req.body as IntegrationTokenType;
+
+ const usersList = await ConfigService.getUsersList();
+ const user = usersList.find((u) => u.username === session.user.name);
+ if (!user) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ const isTokenNameAlreadyExists = user.tokens?.some((t) => t.name === name);
+ if (isTokenNameAlreadyExists) {
+ return ApiResponse.badRequest(res, 'Token name already exists');
+ }
+
+ const newToken: IntegrationTokenType = {
+ token: uuidv4(),
+ name,
+ permissions,
+ creation: getUnixTime(new Date()),
+ };
+
+ const updatedUsersList = usersList.map((u) => {
+ if (u.username === user.username) {
+ u.tokens = u.tokens ? [...u.tokens, newToken] : [newToken];
+ }
+ return u;
+ });
+
+ await ConfigService.updateUsersList(updatedUsersList);
+ return res.status(200).json({ token: newToken.token });
+ } catch (error) {
+ console.log(error);
+ return ApiResponse.serverError(res, error);
+ }
+ } else if (req.method == 'GET') {
+ try {
+ const usersList = await ConfigService.getUsersList();
+ const user = usersList.find((u) => u.username === session.user.name);
+ if (!user) {
+ return ApiResponse.unauthorized(res);
+ }
+ // Send the token list without the token value
+ const tokenList: Omit[] =
+ user.tokens?.map((t) => ({
+ name: t.name,
+ creation: t.creation,
+ permissions: t.permissions,
+ })) || [];
+
+ return res.status(200).json(tokenList);
+ } catch (error) {
+ console.log(error);
+ return ApiResponse.serverError(res, error);
+ }
+ } else if (req.method == 'DELETE') {
+ try {
+ const usersList = await ConfigService.getUsersList();
+ const user = usersList.find((u) => u.username === session.user.name);
+ if (!user) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ const { name } = req.body;
+ if (!name) {
+ return ApiResponse.badRequest(res, 'Missing token name');
+ }
+
+ const isTokenNameExists = user.tokens?.some((t) => t.name === name);
+ if (!isTokenNameExists) {
+ return ApiResponse.badRequest(res, 'Token name not found');
+ }
+
+ const updatedUsersList = usersList.map((u) => {
+ if (u.username === user.username) {
+ u.tokens = u.tokens?.filter((t) => t.name !== name);
+ }
+ return u;
+ });
+
+ await ConfigService.updateUsersList(updatedUsersList);
+ return ApiResponse.success(res, 'Token deleted');
+ } catch (error) {
+ console.log(error);
+ return ApiResponse.serverError;
+ }
+ } else {
+ return ApiResponse.methodNotAllowed(res);
+ }
+}
+
+const validateRequestBody = (
+ req: NextApiRequest & { body: { name: string; permissions: TokenPermissionsType } }
+) => {
+ const { name, permissions } = req.body as { name: string; permissions: TokenPermissionsType };
+ if (!name || !permissions) {
+ throw new Error('Missing required fields');
+ }
+ if (
+ typeof permissions.create !== 'boolean' ||
+ typeof permissions.read !== 'boolean' ||
+ typeof permissions.update !== 'boolean' ||
+ typeof permissions.delete !== 'boolean'
+ ) {
+ throw new Error('Invalid permissions');
+ }
+ const nameRegex = new RegExp('^[a-zA-Z0-9_-]{1,25}$');
+ if (!nameRegex.test(name)) {
+ throw new Error('Your token name is not valid');
+ }
+};
diff --git a/pages/api/v1/notif/apprise/alert.test.ts b/pages/api/v1/notif/apprise/alert.test.ts
new file mode 100644
index 0000000..d0b1a70
--- /dev/null
+++ b/pages/api/v1/notif/apprise/alert.test.ts
@@ -0,0 +1,204 @@
+import { getServerSession } from 'next-auth/next';
+import { createMocks } from 'node-mocks-http';
+import { ConfigService } from '~/services';
+import handler from '~/pages/api/v1/notif/apprise/alert';
+
+vi.mock('next-auth/next');
+vi.mock('~/services');
+
+describe('Get Apprise Alert API', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 405 if the method is not GET', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if the user is not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 400 if the user does not exist', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'nonexistent' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseAlert: true,
+ },
+ ]);
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ });
+
+ it('should return appriseAlert value if the user exists', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseAlert: true,
+ },
+ ]);
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ appriseAlert: true });
+ });
+
+ it('should return 500 if there is an error reading the file', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockImplementation(() => {
+ throw new Error();
+ });
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'API error, contact the administrator',
+ });
+ });
+});
+
+describe('Update Apprise Alert API', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.resetModules();
+ vi.resetAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 405 if the method is not allowed', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if the user is not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+
+ const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 422 if the request body is invalid', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: 'not-boolean' } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(422);
+ expect(res._getJSONData()).toEqual({ message: 'Unexpected data' });
+ });
+
+ it('should return 400 if the user does not exist', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'nonexistent' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseAlert: false,
+ },
+ ]);
+
+ const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ });
+
+ it('should update appriseAlert and return 200 if everything is correct', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseAlert: false,
+ },
+ ]);
+
+ const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } });
+ await handler(req, res);
+
+ expect(ConfigService.updateUsersList).toHaveBeenCalledWith([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseAlert: true,
+ },
+ ]);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ message: 'Successful API send' });
+ });
+
+ it('should return 500 if there is an error reading users file', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'ENOENT' });
+
+ const { req, res } = createMocks({ method: 'PUT', body: { appriseAlert: true } });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({ status: 500, message: 'No such file or directory' });
+ });
+});
diff --git a/pages/api/v1/notif/apprise/alert.ts b/pages/api/v1/notif/apprise/alert.ts
new file mode 100644
index 0000000..b0a5fe8
--- /dev/null
+++ b/pages/api/v1/notif/apprise/alert.ts
@@ -0,0 +1,62 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import { getServerSession } from 'next-auth/next';
+import { ConfigService } from '~/services';
+import { ErrorResponse, AppriseAlertResponse } from '~/types';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import ApiResponse from '~/helpers/functions/apiResponse';
+
+export default async function handler(
+ req: NextApiRequest & { body: { appriseAlert: boolean } },
+ res: NextApiResponse
+) {
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ if (req.method == 'GET') {
+ try {
+ const usersList = await ConfigService.getUsersList();
+
+ //Verify that the user of the session exists
+ const user = usersList.find((u) => u.username === session.user?.name);
+ if (!user) {
+ res.status(400).json({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ return;
+ }
+
+ res.status(200).json({ appriseAlert: user.appriseAlert });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+ } else if (req.method == 'PUT') {
+ const { appriseAlert } = req.body;
+ if (typeof appriseAlert !== 'boolean') {
+ return res.status(422).json({ message: 'Unexpected data' });
+ }
+
+ try {
+ const usersList = await ConfigService.getUsersList();
+ const userIndex = usersList.findIndex((u) => u.username === session.user?.name);
+
+ if (userIndex === -1) {
+ return res
+ .status(400)
+ .json({ message: 'User is incorrect. Please, logout to update your session.' });
+ }
+
+ const updatedUsersList = usersList.map((user, index) =>
+ index === userIndex ? { ...user, appriseAlert } : user
+ );
+
+ await ConfigService.updateUsersList(updatedUsersList);
+ return res.status(200).json({ message: 'Successful API send' });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+ } else {
+ return ApiResponse.methodNotAllowed(res);
+ }
+}
diff --git a/pages/api/v1/notif/apprise/mode.test.ts b/pages/api/v1/notif/apprise/mode.test.ts
new file mode 100644
index 0000000..170b360
--- /dev/null
+++ b/pages/api/v1/notif/apprise/mode.test.ts
@@ -0,0 +1,191 @@
+import { getServerSession } from 'next-auth/next';
+import { createMocks } from 'node-mocks-http';
+import { ConfigService } from '~/services';
+import handler from '~/pages/api/v1/notif/apprise/mode';
+import { AppriseModeEnum } from '~/types/domain/config.types';
+
+vi.mock('next-auth/next');
+vi.mock('~/services');
+
+describe('Get Apprise Mode API', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 405 if the method is not GET', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if the user is not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 400 if the user does not exist', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'nonexistent' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseMode: AppriseModeEnum.STATELESS,
+ appriseStatelessURL: 'https://example.com',
+ },
+ ]);
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ });
+
+ it('should return appriseMode and appriseStatelessURL if the user exists', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseMode: AppriseModeEnum.STATELESS,
+ appriseStatelessURL: 'https://example.com',
+ },
+ ]);
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ appriseMode: 'stateless',
+ appriseStatelessURL: 'https://example.com',
+ });
+ });
+
+ it('should return 500 if there is an error reading the file', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockImplementation(() => {
+ throw new Error();
+ });
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'API error, contact the administrator',
+ });
+ });
+});
+
+describe('Apprise Mode Update API', () => {
+ it('should return 405 if method is not allowed', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+
+ const { req, res } = createMocks({ method: 'PUT' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 422 if invalid data is provided', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testuser' } });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { appriseMode: 'invalid-mode', appriseStatelessURL: 'https://example.com' },
+ });
+
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(422);
+ });
+
+ it('should return 400 if user does not exist', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'unknownuser' } });
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseMode: AppriseModeEnum.PACKAGE,
+ },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { appriseMode: 'stateless', appriseStatelessURL: 'https://example.com' },
+ });
+
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(400);
+ });
+
+ it('should update user settings and return 200', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'testuser' } });
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseMode: AppriseModeEnum.PACKAGE,
+ },
+ ]);
+ vi.mocked(ConfigService.updateUsersList).mockResolvedValue();
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { appriseMode: 'stateless', appriseStatelessURL: 'https://example.com' },
+ });
+
+ await handler(req, res);
+
+ expect(ConfigService.updateUsersList).toHaveBeenCalledWith([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseMode: AppriseModeEnum.STATELESS,
+ appriseStatelessURL: 'https://example.com',
+ },
+ ]);
+ expect(res._getStatusCode()).toBe(200);
+ });
+});
diff --git a/pages/api/v1/notif/apprise/mode.ts b/pages/api/v1/notif/apprise/mode.ts
new file mode 100644
index 0000000..4223390
--- /dev/null
+++ b/pages/api/v1/notif/apprise/mode.ts
@@ -0,0 +1,65 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import { getServerSession } from 'next-auth/next';
+import { ConfigService } from '~/services';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { AppriseModeDTO, ErrorResponse } from '~/types';
+import ApiResponse from '~/helpers/functions/apiResponse';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ if (req.method == 'GET') {
+ try {
+ const usersList = await ConfigService.getUsersList();
+
+ //Verify that the user of the session exists
+ const user = usersList.find((u) => u.username === session.user?.name);
+ if (!user) {
+ res.status(400).json({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ return;
+ }
+ res.status(200).json({
+ appriseMode: user.appriseMode,
+ appriseStatelessURL: user.appriseStatelessURL,
+ });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+ } else if (req.method == 'PUT') {
+ const { appriseMode, appriseStatelessURL } = req.body;
+
+ if (!['package', 'stateless'].includes(appriseMode)) {
+ return res.status(422).json({ message: 'Unexpected data' });
+ }
+
+ try {
+ const usersList = await ConfigService.getUsersList();
+ const userIndex = usersList.findIndex((user) => user.username === session.user?.name);
+
+ if (userIndex === -1) {
+ return res.status(400).json({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ }
+
+ const updatedUsersList = usersList.map((user, index) =>
+ index === userIndex ? { ...user, appriseMode, appriseStatelessURL } : user
+ );
+
+ await ConfigService.updateUsersList(updatedUsersList);
+ return res.status(200).json({ message: 'Successful API send' });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+ } else {
+ return ApiResponse.methodNotAllowed(res);
+ }
+}
diff --git a/pages/api/v1/notif/apprise/services.test.ts b/pages/api/v1/notif/apprise/services.test.ts
new file mode 100644
index 0000000..a079bdc
--- /dev/null
+++ b/pages/api/v1/notif/apprise/services.test.ts
@@ -0,0 +1,223 @@
+import { getServerSession } from 'next-auth/next';
+import { createMocks } from 'node-mocks-http';
+import { ConfigService } from '~/services';
+import handler from '~/pages/api/v1/notif/apprise/services';
+
+vi.mock('next-auth/next');
+vi.mock('~/services');
+
+describe('Get Apprise Services API', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 405 if the method is not GET', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if the user is not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 400 if the user does not exist', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'nonexistent' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseServices: ['service1', 'service2'],
+ },
+ ]);
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ });
+
+ it('should return appriseServices if the user exists', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ password: 'hashedpassword',
+ roles: ['user'],
+ email: 'testuser@example.com',
+ appriseServices: ['service1', 'service2'],
+ },
+ ]);
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ appriseServices: ['service1', 'service2'],
+ });
+ });
+
+ it('should return 500 if there is an error reading the file', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockImplementation(() => {
+ throw new Error();
+ });
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'API error, contact the administrator',
+ });
+ });
+});
+
+describe('PUT Apprise services API', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.resetModules();
+ vi.resetAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 401 if not authenticated', async () => {
+ // Mock unauthenticated session
+ vi.mocked(getServerSession).mockResolvedValue(null);
+
+ const { req, res } = createMocks({ method: 'PUT' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 405 if method is not handling', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+ const { req, res } = createMocks({ method: 'DELETE' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 400 if user is not found in the users list', async () => {
+ // Mock authenticated session
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'Ada',
+ password: 'securepassword',
+ roles: ['user'],
+ email: 'ada@example.com',
+ },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { appriseURLs: 'http://example.com' },
+ });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ });
+
+ it('should return 200 and successfully update the appriseURLs', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'Lovelace',
+ password: 'securepassword',
+ roles: ['user'],
+ email: 'lovelace@example.com',
+ appriseServices: [],
+ },
+ ]);
+ vi.mocked(ConfigService.updateUsersList).mockResolvedValue();
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { appriseURLs: 'http://example.com\nhttp://anotherurl.com' },
+ });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ message: 'Successful API send' });
+ });
+
+ it('should return 500 if there is an error reading the users file', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'ENOENT' });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { appriseURLs: 'http://example.com' },
+ });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'No such file or directory',
+ });
+ });
+
+ it('should return 500 if there is an API error', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'UNKNOWN_ERROR' });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { appriseURLs: 'http://example.com' },
+ });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'API error, contact the administrator',
+ });
+ });
+});
diff --git a/pages/api/v1/notif/apprise/services.ts b/pages/api/v1/notif/apprise/services.ts
new file mode 100644
index 0000000..7b13971
--- /dev/null
+++ b/pages/api/v1/notif/apprise/services.ts
@@ -0,0 +1,67 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import { getServerSession } from 'next-auth/next';
+import { ConfigService } from '~/services';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { AppriseServicesDTO, ErrorResponse } from '~/types';
+import ApiResponse from '~/helpers/functions/apiResponse';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ if (req.method == 'GET') {
+ try {
+ const usersList = await ConfigService.getUsersList();
+
+ //Verify that the user of the session exists
+ const user = usersList.find((u) => u.username === session.user?.name);
+ if (!user) {
+ res.status(400).json({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ return;
+ }
+
+ res.status(200).json({
+ appriseServices: user.appriseServices,
+ });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+ } else if (req.method == 'PUT') {
+ const { appriseURLs } = req.body;
+
+ try {
+ const usersList = await ConfigService.getUsersList();
+ const userIndex = usersList.findIndex((user) => user.username === session.user?.name);
+
+ if (userIndex === -1) {
+ return res.status(400).json({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ }
+
+ //Build the services URLs list from form
+ const appriseURLsArray = appriseURLs
+ .replace(/ /g, '')
+ .split('\n')
+ .filter((el: string) => el != '');
+
+ const updatedUsersList = usersList.map((user, index) =>
+ index === userIndex ? { ...user, appriseServices: appriseURLsArray } : user
+ );
+
+ await ConfigService.updateUsersList(updatedUsersList);
+ return res.status(200).json({ message: 'Successful API send' });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+ } else {
+ return ApiResponse.methodNotAllowed(res);
+ }
+}
diff --git a/pages/api/v1/notif/apprise/test.ts b/pages/api/v1/notif/apprise/test.ts
new file mode 100644
index 0000000..28e0fe0
--- /dev/null
+++ b/pages/api/v1/notif/apprise/test.ts
@@ -0,0 +1,75 @@
+import { exec } from 'child_process';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { getServerSession } from 'next-auth/next';
+import { promisify } from 'util';
+import { ConfigService } from '~/services';
+import { ErrorResponse, SuccessResponse } from '~/types';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+
+const execAsync = promisify(exec);
+
+const getAppriseServicesURLs = (services: string[]): string => services.join(' ');
+
+const checkAppriseInstalled = async (): Promise => {
+ try {
+ await execAsync('apprise -V');
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+const sendNotificationViaPackage = async (urls: string) => {
+ const command = `apprise -v -b "This is a test notification from BorgWarehouse !" ${urls}`;
+ return execAsync(command);
+};
+
+const sendNotificationViaStateless = async (statelessURL: string, urls: string) => {
+ const response = await fetch(`${statelessURL}/notify`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ urls, body: 'This is a test notification from BorgWarehouse !' }),
+ });
+ if (!response.ok) throw new Error(response.statusText);
+};
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ if (req.method !== 'POST') return res.status(405).json({ message: 'Bad request on API' });
+
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) return res.status(401).json({ message: 'You must be logged in.' });
+
+ try {
+ const usersList = await ConfigService.getUsersList();
+ const user = usersList.find((u) => u.username === session.user?.name);
+ if (!user)
+ return res.status(400).json({ message: 'Invalid user session. Please log out and retry.' });
+
+ if (!user.appriseServices || user.appriseServices.length === 0) {
+ return res.status(422).json({ message: 'You must provide at least one Apprise URL.' });
+ }
+
+ const appriseServicesURLs = getAppriseServicesURLs(user.appriseServices);
+
+ if (user.appriseMode === 'package') {
+ if (!(await checkAppriseInstalled())) {
+ return res.status(500).json({ message: 'Apprise is not installed as a local package.' });
+ }
+ await sendNotificationViaPackage(appriseServicesURLs);
+ } else if (user.appriseMode === 'stateless') {
+ if (!user.appriseStatelessURL) {
+ return res.status(500).json({ message: 'Please provide an Apprise stateless API URL.' });
+ }
+ await sendNotificationViaStateless(user.appriseStatelessURL, appriseServicesURLs);
+ } else {
+ return res.status(422).json({ message: 'Invalid or unsupported Apprise mode.' });
+ }
+
+ return res.status(200).json({ message: 'Notifications successfully sent.' });
+ } catch (error) {
+ return res.status(500).json({ message: `${error}` });
+ }
+}
diff --git a/pages/api/v1/notif/email/alert.test.ts b/pages/api/v1/notif/email/alert.test.ts
new file mode 100644
index 0000000..a52d248
--- /dev/null
+++ b/pages/api/v1/notif/email/alert.test.ts
@@ -0,0 +1,241 @@
+import { getServerSession } from 'next-auth/next';
+import { createMocks } from 'node-mocks-http';
+import handler from '~/pages/api/v1/notif/email/alert';
+import { ConfigService } from '~/services';
+
+vi.mock('next-auth/next');
+vi.mock('~/services');
+
+describe('Get Email Alert API', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 405 if the method is not GET', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if the user is not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 400 if the user does not exist', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'nonexistent' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ email: 'testuser@example.com',
+ password: 'hashedpassword',
+ roles: ['user'],
+ emailAlert: true,
+ },
+ ]);
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ });
+
+ it('should return emailAlert if the user exists', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testuser',
+ email: 'testuser@example.com',
+ password: 'hashedpassword',
+ roles: ['user'],
+ emailAlert: true,
+ },
+ ]);
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ emailAlert: true,
+ });
+ });
+
+ it('should return 500 if there is an error reading the file', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockImplementation(() => {
+ throw new Error();
+ });
+
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'API error, contact the administrator',
+ });
+ });
+});
+
+describe('Update email Alert API', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 401 if not authenticated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+
+ const { req, res } = createMocks({ method: 'PUT' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 405 if method is not PUT', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'testuser' },
+ });
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 422 if emailAlert is not a boolean', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { emailAlert: 'yes' }, // incorrect type
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(422);
+ expect(res._getJSONData()).toEqual({ message: 'Unexpected data' });
+ });
+
+ it('should return 400 if user is not found in the users list', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 2,
+ username: 'Ada',
+ email: 'ada@example.com',
+ emailAlert: false,
+ password: '',
+ roles: [],
+ },
+ ]);
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { emailAlert: true },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ });
+
+ it('should update emailAlert and return 200 on success', async () => {
+ const user = {
+ id: 1,
+ username: 'Lovelace',
+ email: 'lovelace@example.com',
+ emailAlert: false,
+ password: '',
+ roles: [],
+ };
+
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([user]);
+ vi.mocked(ConfigService.updateUsersList).mockResolvedValue();
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { emailAlert: true },
+ });
+
+ await handler(req, res);
+
+ expect(ConfigService.updateUsersList).toHaveBeenCalledWith([{ ...user, emailAlert: true }]);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ message: 'Successful API send' });
+ });
+
+ it('should return 500 if there is a file system error (ENOENT)', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'ENOENT' });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { emailAlert: true },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'No such file or directory',
+ });
+ });
+
+ it('should return 500 on unknown error', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'Lovelace' },
+ });
+
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue({ code: 'UNKNOWN' });
+
+ const { req, res } = createMocks({
+ method: 'PUT',
+ body: { emailAlert: true },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'API error, contact the administrator',
+ });
+ });
+});
diff --git a/pages/api/v1/notif/email/alert.ts b/pages/api/v1/notif/email/alert.ts
new file mode 100644
index 0000000..c1e4937
--- /dev/null
+++ b/pages/api/v1/notif/email/alert.ts
@@ -0,0 +1,61 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import { getServerSession } from 'next-auth/next';
+import { ConfigService } from '~/services';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { EmailAlertDTO, ErrorResponse } from '~/types';
+import ApiResponse from '~/helpers/functions/apiResponse';
+
+export default async function handler(
+ req: NextApiRequest & { body: EmailAlertDTO },
+ res: NextApiResponse
+) {
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return ApiResponse.unauthorized(res);
+ }
+ if (req.method == 'GET') {
+ try {
+ const usersList = await ConfigService.getUsersList();
+
+ //Verify that the user of the session exists
+ const user = usersList.find((u) => u.username === session.user?.name);
+ if (!user) {
+ res.status(400).json({
+ message: 'User is incorrect. Please, logout to update your session.',
+ });
+ return;
+ }
+ res.status(200).json({ emailAlert: user.emailAlert });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+ } else if (req.method == 'PUT') {
+ const { emailAlert } = req.body;
+
+ if (typeof emailAlert !== 'boolean') {
+ return res.status(422).json({ message: 'Unexpected data' });
+ }
+
+ try {
+ const usersList = await ConfigService.getUsersList();
+ const userIndex = usersList.findIndex((u) => u.username === session.user?.name);
+
+ if (userIndex === -1) {
+ return res
+ .status(400)
+ .json({ message: 'User is incorrect. Please, logout to update your session.' });
+ }
+
+ const updatedUsersList = usersList.map((user, index) =>
+ index === userIndex ? { ...user, emailAlert } : user
+ );
+
+ await ConfigService.updateUsersList(updatedUsersList);
+ return res.status(200).json({ message: 'Successful API send' });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+ } else {
+ return ApiResponse.methodNotAllowed(res);
+ }
+}
diff --git a/pages/api/v1/notif/email/test.test.ts b/pages/api/v1/notif/email/test.test.ts
new file mode 100644
index 0000000..286f125
--- /dev/null
+++ b/pages/api/v1/notif/email/test.test.ts
@@ -0,0 +1,46 @@
+import { createMocks } from 'node-mocks-http';
+import handler from '~/pages/api/v1/notif/email/test';
+import { getServerSession } from 'next-auth/next';
+
+vi.mock('next-auth/next');
+vi.mock('~/services', () => ({
+ NotifService: {
+ nodemailerSMTP: vi.fn(() => ({
+ sendMail: vi.fn().mockResolvedValue({ messageId: 'fake-message-id' }),
+ })),
+ },
+}));
+
+describe('Email API', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 401 if not authenticated', async () => {
+ // Mock unauthenticated session
+ vi.mocked(getServerSession).mockResolvedValue(null);
+
+ // Simulate a POST request
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+
+ // Expect 401 unauthorized
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should send an email if authenticated', async () => {
+ // Mock unauthenticated session
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { email: 'ada-lovelace@example.com', name: 'Lovelace' },
+ });
+
+ // Simulate a POST request
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+
+ // Expect 200 and a success message
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ message: 'Mail successfully sent' });
+ });
+});
diff --git a/pages/api/v1/notif/email/test.ts b/pages/api/v1/notif/email/test.ts
new file mode 100644
index 0000000..a4c78a9
--- /dev/null
+++ b/pages/api/v1/notif/email/test.ts
@@ -0,0 +1,31 @@
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { NextApiRequest, NextApiResponse } from 'next';
+import emailTest from '~/helpers/templates/emailTest';
+import { NotifService } from '~/services';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== 'POST') {
+ return res.status(405);
+ }
+
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ return res.status(401);
+ }
+
+ try {
+ const transporter = NotifService.nodemailerSMTP();
+ if (!session.user?.email || !session.user?.name) {
+ return res.status(400).json({ message: 'User email or name is missing' });
+ }
+ const mailData = emailTest(session.user.email, session.user.name);
+ const info = await transporter.sendMail(mailData);
+ console.log(info);
+
+ return res.status(200).json({ message: 'Mail successfully sent' });
+ } catch (error) {
+ console.log(error);
+ return res.status(500).json({ message: `An error occurred while sending the email: ${error}` });
+ }
+}
diff --git a/pages/api/v1/repositories/[slug]/index.test.ts b/pages/api/v1/repositories/[slug]/index.test.ts
new file mode 100644
index 0000000..1a673fc
--- /dev/null
+++ b/pages/api/v1/repositories/[slug]/index.test.ts
@@ -0,0 +1,573 @@
+import { createMocks } from 'node-mocks-http';
+import handler from '~/pages/api/v1/repositories/[slug]';
+import { getServerSession } from 'next-auth/next';
+import { ConfigService, AuthService, ShellService } from '~/services';
+import { isSshPubKeyDuplicate } from '~/helpers/functions';
+import { Repository } from '~/types/domain/config.types';
+
+vi.mock('next-auth/next', () => ({
+ getServerSession: vi.fn(),
+}));
+vi.mock('~/helpers/functions', () => ({
+ isSshPubKeyDuplicate: vi.fn(),
+}));
+vi.mock('~/services');
+
+const mockRepoList: Repository[] = [
+ {
+ id: 1,
+ alias: 'repo1',
+ repositoryName: 'abcd1234',
+ status: true,
+ lastSave: 1678901234,
+ alert: 1,
+ storageSize: 100,
+ storageUsed: 50,
+ sshPublicKey: 'ssh-rsa AAAAB3Nza...fakekey1',
+ comment: 'Test repository 1',
+ displayDetails: true,
+ unixUser: 'user1',
+ lanCommand: false,
+ appendOnlyMode: false,
+ lastStatusAlertSend: 1678901234,
+ },
+ {
+ id: 2,
+ alias: 'repo2',
+ repositoryName: 'cdef5678',
+ status: false,
+ lastSave: 1678905678,
+ storageSize: 200,
+ storageUsed: 150,
+ sshPublicKey: 'ssh-rsa AAAAB3Nza...fakekey2',
+ comment: 'Test repository 2',
+ displayDetails: false,
+ unixUser: 'user2',
+ },
+];
+
+describe('Repository GET by repositoryName', () => {
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.resetModules();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 405 if method is not handling', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ const { req, res } = createMocks({ method: 'POST', query: { slug: '1234abcd' } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if no session or authorization header is provided', async () => {
+ const { req, res } = createMocks({ method: 'GET', query: { slug: '1234abcd' } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 401 if API key is invalid', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue(undefined);
+ const { req, res } = createMocks({
+ method: 'GET',
+ query: { slug: '1234abcd' },
+ headers: { authorization: 'Bearer INVALID_API_KEY' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 403 if API key does not have read permissions', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue({
+ read: false,
+ update: true,
+ create: true,
+ delete: true,
+ });
+ const { req, res } = createMocks({
+ method: 'GET',
+ query: { slug: '1234abcd' },
+ headers: { authorization: 'Bearer API_KEY' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(403);
+ });
+
+ it('should return 400 if slug is missing or malformed', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ const { req, res } = createMocks({ method: 'GET', query: { slug: undefined } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(400);
+ });
+
+ it('should return 404 if repository is not found', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
+ const { req, res } = createMocks({ method: 'GET', query: { slug: 'def1234a' } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(404);
+ });
+
+ it('should return 200 and the repository data if found', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue(mockRepoList);
+ const { req, res } = createMocks({ method: 'GET', query: { slug: 'abcd1234' } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ repo: mockRepoList[0] });
+ });
+});
+
+describe('Repository PATCH by repositoryName', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.resetAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 405 if method is not handling', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ const { req, res } = createMocks({ method: 'POST', query: { slug: 'abcd1234' } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if no session or authorization header is provided', async () => {
+ const { req, res } = createMocks({ method: 'PATCH', query: { slug: 'abcd1234' } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 401 if API key is invalid', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue(undefined);
+ const { req, res } = createMocks({
+ method: 'PATCH',
+ query: { slug: 'abcd1234' },
+ headers: { authorization: 'Bearer INVALID_API_KEY' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 403 if API key does not have update permissions', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue({
+ update: false,
+ create: false,
+ delete: false,
+ read: false,
+ });
+ const { req, res } = createMocks({
+ method: 'PATCH',
+ query: { slug: 'abcd1234' },
+ headers: { authorization: 'Bearer API_KEY' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(403);
+ });
+
+ it('should return 400 if slug is missing or malformed', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ const { req, res } = createMocks({ method: 'PATCH', query: { slug: undefined } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(400);
+ });
+
+ it('should return 404 if repository is not found', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
+ const { req, res } = createMocks({ method: 'PATCH', query: { slug: '1234edfe' } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(404);
+ });
+
+ it('should return 409 if SSH key is duplicated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'USER' },
+ });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue(mockRepoList);
+ vi.mocked(isSshPubKeyDuplicate).mockReturnValue(true);
+ const { req, res } = createMocks({
+ method: 'PATCH',
+ query: { slug: 'abcd1234' },
+ body: { sshPublicKey: 'duplicate-key' },
+ });
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(409);
+ });
+
+ it('should return 500 if updateRepoShell fails', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ id: 123,
+ repositoryName: 'a43928f3',
+ alias: 'test-alias',
+ status: true,
+ lastSave: 0,
+ storageSize: 100,
+ storageUsed: 50,
+ lanCommand: false,
+ sshPublicKey: 'test-key',
+ comment: 'Test repository',
+ },
+ ]);
+ vi.mocked(ShellService.updateRepo).mockRejectedValue(new Error('Failed to update repository'));
+ const { req, res } = createMocks({
+ method: 'PATCH',
+ query: { slug: 'a43928f3' },
+ body: { alias: 'new-alias' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'Failed to update repository',
+ });
+ expect(ConfigService.updateRepoList).not.toHaveBeenCalled();
+ });
+
+ it('should successfully update repository with a session', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ id: 123,
+ repositoryName: '345f6789',
+ alias: 'test-alias',
+ status: true,
+ lastSave: 0,
+ storageSize: 100,
+ storageUsed: 50,
+ lanCommand: false,
+ sshPublicKey: 'test-key',
+ comment: 'Test repository',
+ },
+ ]);
+ vi.mocked(ShellService.updateRepo).mockResolvedValue({ stderr: '', stdout: '' });
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue();
+ const { req, res } = createMocks({
+ method: 'PATCH',
+ query: { slug: '345f6789' },
+ body: { alias: 'new-alias' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ status: 200,
+ message: 'Repository 345f6789 has been edited',
+ });
+ });
+
+ it('should successfully update repository with API key', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue({
+ update: true,
+ create: false,
+ delete: false,
+ read: false,
+ });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ id: 456,
+ repositoryName: '345f6789',
+ alias: 'repo-alias',
+ status: true,
+ lastSave: 0,
+ storageSize: 100,
+ storageUsed: 50,
+ lanCommand: false,
+ sshPublicKey: 'ssh-key',
+ comment: 'Test repository',
+ },
+ ]);
+ vi.mocked(ShellService.updateRepo).mockResolvedValue({ stderr: '', stdout: '' });
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue();
+ const { req, res } = createMocks({
+ method: 'PATCH',
+ query: { slug: '345f6789' },
+ headers: { authorization: 'Bearer API_KEY' },
+ body: { alias: 'updated-repo' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ status: 200,
+ message: 'Repository 345f6789 has been edited',
+ });
+ });
+
+ it('should only update the provided fields, keep the rest unchanged and history the modification.', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ id: 123,
+ repositoryName: '345f6e89',
+ alias: 'old-alias',
+ sshPublicKey: 'old-key',
+ storageSize: 100,
+ lanCommand: false,
+ status: true,
+ lastSave: 0,
+ storageUsed: 50,
+ comment: 'Initial repository setup',
+ },
+ ]);
+ vi.mocked(ShellService.updateRepo).mockResolvedValue({ stderr: '', stdout: '' });
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue();
+ const { req, res } = createMocks({
+ method: 'PATCH',
+ query: { slug: '345f6e89' },
+ body: {
+ alias: 'new-alias',
+ sshPublicKey: 'new-key',
+ comment: 'new-comment',
+ alert: 0,
+ appendOnlyMode: true,
+ },
+ });
+ await handler(req, res);
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith(
+ [
+ {
+ id: 123,
+ repositoryName: '345f6e89',
+ alias: 'new-alias',
+ sshPublicKey: 'new-key',
+ storageSize: 100,
+ lanCommand: false,
+ comment: 'new-comment',
+ status: true,
+ lastSave: 0,
+ appendOnlyMode: true,
+ alert: 0,
+ storageUsed: 50,
+ },
+ ],
+ true
+ );
+ expect(ShellService.updateRepo).toHaveBeenCalledWith('345f6e89', 'new-key', 100, true);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ status: 200,
+ message: 'Repository 345f6e89 has been edited',
+ });
+ });
+});
+
+describe('Repository DELETE by repositoryName', () => {
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.resetModules();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 405 if method is not handling', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'USER' },
+ });
+ const { req, res } = createMocks({ method: 'POST', query: { slug: 'abcd1234' } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if no session or authorization header is provided', async () => {
+ const { req, res } = createMocks({ method: 'DELETE' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 403 if deletion is disabled via environment variable', async () => {
+ process.env.DISABLE_DELETE_REPO = 'true';
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'USER' },
+ });
+ const { req, res } = createMocks({ method: 'DELETE', query: { slug: 'abcd1234' } });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(403);
+ expect(res._getJSONData()).toEqual({
+ status: 403,
+ message: 'Deletion is disabled on this server',
+ });
+ delete process.env.DISABLE_DELETE_REPO;
+ });
+
+ it('should return 400 if slug is missing or malformed', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'USER' },
+ });
+ const { req, res } = createMocks({
+ method: 'DELETE',
+ query: { slug: undefined },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(400);
+ expect(res._getJSONData()).toEqual({
+ status: 400,
+ message: 'Slug must be a valid repository name (8-character hexadecimal string)',
+ });
+ });
+
+ it('should return 404 if repository is not found', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'USER' },
+ });
+ const { req, res } = createMocks({
+ method: 'DELETE',
+ query: { slug: 'abcd1234' },
+ });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(404);
+ expect(res._getJSONData()).toEqual({
+ status: 404,
+ message: 'Repository with name abcd1234 not found',
+ });
+ });
+
+ it('should return 500 if deleteRepo fails', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'USER' },
+ });
+ const { req, res } = createMocks({
+ method: 'DELETE',
+ query: { slug: 'abde3421' },
+ });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ id: 123,
+ repositoryName: 'abde3421',
+ alias: 'test-alias',
+ status: true,
+ lastSave: 0,
+ storageSize: 1024,
+ storageUsed: 512,
+ sshPublicKey: 'ssh-rsa AAAAB3Nz',
+ comment: 'Test repository',
+ },
+ ]);
+ vi.mocked(ShellService.deleteRepo).mockRejectedValue(new Error('Failed to delete repository'));
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(500);
+ expect(res._getJSONData()).toEqual({
+ status: 500,
+ message: 'Failed to delete repository',
+ });
+ expect(ConfigService.updateRepoList).not.toHaveBeenCalled();
+ });
+
+ it('should delete the repository and return 200 on success with a session', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({
+ user: { name: 'USER' },
+ });
+ const { req, res } = createMocks({
+ method: 'DELETE',
+ query: { slug: 'abde3421' },
+ });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ id: 1234,
+ repositoryName: 'abde3421',
+ alias: 'test-alias',
+ status: true,
+ lastSave: 0,
+ storageSize: 1024,
+ storageUsed: 512,
+ sshPublicKey: 'ssh-rsa AAAAB3Nz',
+ comment: 'Test repository',
+ },
+ ]);
+ vi.mocked(ShellService.deleteRepo).mockResolvedValue({ stderr: '', stdout: '' });
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue();
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ status: 200,
+ message: 'Repository abde3421 deleted',
+ });
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith([], true);
+ });
+
+ it('should delete the repository and return 200 on success with an API key', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue({
+ delete: true,
+ read: true,
+ create: true,
+ update: true,
+ });
+ const { req, res } = createMocks({
+ method: 'DELETE',
+ query: { slug: 'abde3421' },
+ headers: {
+ authorization: 'Bearer API_KEY',
+ },
+ });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ id: 12345,
+ repositoryName: 'abde3421',
+ alias: 'test-alias',
+ status: true,
+ lastSave: 0,
+ storageSize: 1024,
+ storageUsed: 512,
+ sshPublicKey: 'ssh-rsa AAAAB3Nz',
+ comment: 'Test repository',
+ },
+ ]);
+ vi.mocked(ShellService.deleteRepo).mockResolvedValue({ stdout: 'delete', stderr: '' });
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue();
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({
+ status: 200,
+ message: 'Repository abde3421 deleted',
+ });
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith([], true);
+ });
+
+ it('should return 401 if the API key is invalid', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue(undefined);
+ const { req, res } = createMocks({
+ method: 'DELETE',
+ query: { slug: 'abde3421' },
+ headers: {
+ authorization: 'Bearer API_KEY',
+ },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ expect(res._getJSONData()).toEqual({
+ status: 401,
+ message: 'Invalid API key',
+ });
+ });
+
+ it('should return 403 if the API key does not have delete permissions', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue({
+ delete: false,
+ read: true,
+ create: true,
+ update: true,
+ });
+ const { req, res } = createMocks({
+ method: 'DELETE',
+ query: { slug: 'abde3421' },
+ headers: {
+ authorization: 'Bearer API_KEY',
+ },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(403);
+ expect(res._getJSONData()).toEqual({
+ status: 403,
+ message: 'Insufficient permissions',
+ });
+ });
+});
diff --git a/pages/api/v1/repositories/[slug]/index.ts b/pages/api/v1/repositories/[slug]/index.ts
new file mode 100644
index 0000000..283ac86
--- /dev/null
+++ b/pages/api/v1/repositories/[slug]/index.ts
@@ -0,0 +1,193 @@
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { BorgWarehouseApiResponse, Repository } from '~/types';
+import ApiResponse from '~/helpers/functions/apiResponse';
+import { ConfigService, AuthService, ShellService } from '~/services';
+import { isSshPubKeyDuplicate } from '~/helpers/functions';
+import repositoryNameCheck from '~/helpers/functions/repositoryNameCheck';
+
+export default async function handler(
+ req: NextApiRequest & { body: Partial },
+ res: NextApiResponse
+) {
+ const session = await getServerSession(req, res, authOptions);
+ const { authorization } = req.headers;
+ if (!session && !authorization) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ // Validate slug
+ const slug = Array.isArray(req.query.slug) ? req.query.slug[0] : req.query.slug;
+ if (!slug || !repositoryNameCheck(slug)) {
+ return ApiResponse.badRequest(
+ res,
+ 'Slug must be a valid repository name (8-character hexadecimal string)'
+ );
+ }
+
+ if (req.method == 'GET') {
+ try {
+ if (!session && authorization) {
+ const permissions = await AuthService.tokenController(req.headers);
+ if (!permissions) {
+ return ApiResponse.unauthorized(res, 'Invalid API key');
+ }
+ if (!permissions.read) {
+ return ApiResponse.forbidden(res, 'Insufficient permissions');
+ }
+ }
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+
+ try {
+ const repoList = await ConfigService.getRepoList();
+
+ const repo = repoList.find((repo) => repo.repositoryName === slug);
+ if (!repo) {
+ return ApiResponse.notFound(res, 'No repository with name ' + slug);
+ }
+
+ return res.status(200).json({ repo });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+ } else if (req.method == 'PATCH') {
+ try {
+ if (!session && authorization) {
+ const permissions = await AuthService.tokenController(req.headers);
+ if (!permissions) {
+ return ApiResponse.unauthorized(res, 'Invalid API key');
+ }
+ if (!permissions.update) {
+ return ApiResponse.forbidden(res, 'Insufficient permissions');
+ }
+ }
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+
+ try {
+ validatePatchRequestBody(req);
+ } catch (error) {
+ if (error instanceof Error) {
+ return ApiResponse.badRequest(res, error.message);
+ }
+ return ApiResponse.badRequest(res, 'Invalid request data');
+ }
+
+ try {
+ const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } =
+ req.body;
+ const repoList = await ConfigService.getRepoList();
+ const repo = repoList.find((repo) => repo.repositoryName === slug);
+ if (!repo) {
+ return ApiResponse.notFound(res, 'Repository not found');
+ }
+
+ const filteredRepoList = repoList.filter((repo) => repo.repositoryName !== slug);
+ if (sshPublicKey && isSshPubKeyDuplicate(sshPublicKey, filteredRepoList)) {
+ return res.status(409).json({
+ status: 409,
+ message:
+ 'The SSH key is already used in another repository. Please use another key or delete the key from the other repository.',
+ });
+ }
+
+ const updatedRepo: Repository = {
+ ...repo,
+ alias: alias ?? repo.alias,
+ sshPublicKey: sshPublicKey ?? repo.sshPublicKey,
+ storageSize: storageSize ?? repo.storageSize,
+ comment: comment ?? repo.comment,
+ alert: alert ?? repo.alert,
+ lanCommand: lanCommand ?? repo.lanCommand,
+ appendOnlyMode: appendOnlyMode ?? repo.appendOnlyMode,
+ };
+
+ await ShellService.updateRepo(
+ updatedRepo.repositoryName,
+ updatedRepo.sshPublicKey,
+ updatedRepo.storageSize,
+ updatedRepo.appendOnlyMode ?? false
+ );
+
+ const updatedRepoList = [...filteredRepoList, updatedRepo];
+ await ConfigService.updateRepoList(updatedRepoList, true);
+
+ return res
+ .status(200)
+ .json({ status: 200, message: `Repository ${repo.repositoryName} has been edited` });
+ } catch (error) {
+ console.log(error);
+ return ApiResponse.serverError(res, error as string);
+ }
+ } else if (req.method == 'DELETE') {
+ try {
+ if (!session && authorization) {
+ const permissions = await AuthService.tokenController(req.headers);
+ if (!permissions) {
+ return ApiResponse.unauthorized(res, 'Invalid API key');
+ }
+ if (!permissions.delete) {
+ return ApiResponse.forbidden(res, 'Insufficient permissions');
+ }
+ }
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+
+ if (process.env.DISABLE_DELETE_REPO === 'true') {
+ return ApiResponse.forbidden(res, 'Deletion is disabled on this server');
+ }
+
+ try {
+ const repoList = await ConfigService.getRepoList();
+
+ const indexToDelete = repoList.map((repo) => repo.repositoryName).indexOf(slug);
+ if (indexToDelete === -1) {
+ return ApiResponse.notFound(res, 'Repository with name ' + slug + ' not found');
+ }
+
+ await ShellService.deleteRepo(repoList[indexToDelete].repositoryName);
+
+ const updatedRepoList = repoList.filter((repo) => repo.repositoryName !== slug);
+
+ await ConfigService.updateRepoList(updatedRepoList, true);
+ return ApiResponse.success(
+ res,
+ `Repository ${repoList[indexToDelete].repositoryName} deleted`
+ );
+ } catch (error) {
+ console.log(error);
+ return ApiResponse.serverError(res, error);
+ }
+ } else {
+ return ApiResponse.methodNotAllowed(res);
+ }
+}
+
+const validatePatchRequestBody = (req: NextApiRequest) => {
+ if (req.body.alias !== undefined && typeof req.body.alias !== 'string') {
+ throw new Error('Alias must be a string');
+ }
+ if (req.body.sshPublicKey !== undefined && typeof req.body.sshPublicKey !== 'string') {
+ throw new Error('SSH Public Key must be a string');
+ }
+ if (req.body.storageSize !== undefined && typeof req.body.storageSize !== 'number') {
+ throw new Error('Storage Size must be a number');
+ }
+ if (req.body.comment !== undefined && typeof req.body.comment !== 'string') {
+ throw new Error('Comment must be a string');
+ }
+ if (req.body.alert !== undefined && typeof req.body.alert !== 'number') {
+ throw new Error('Alert must be a number');
+ }
+ if (req.body.lanCommand !== undefined && typeof req.body.lanCommand !== 'boolean') {
+ throw new Error('Lan Command must be a boolean');
+ }
+ if (req.body.appendOnlyMode !== undefined && typeof req.body.appendOnlyMode !== 'boolean') {
+ throw new Error('Append Only Mode must be a boolean');
+ }
+};
diff --git a/pages/api/v1/repositories/index.test.ts b/pages/api/v1/repositories/index.test.ts
new file mode 100644
index 0000000..ebbb94f
--- /dev/null
+++ b/pages/api/v1/repositories/index.test.ts
@@ -0,0 +1,327 @@
+import { createMocks } from 'node-mocks-http';
+import handler from '~/pages/api/v1/repositories';
+import { getServerSession } from 'next-auth/next';
+import { ConfigService, AuthService, ShellService } from '~/services';
+import { Repository } from '~/types';
+import { isSshPubKeyDuplicate } from '~/helpers/functions';
+
+vi.mock('next-auth/next', () => ({
+ getServerSession: vi.fn(),
+}));
+vi.mock('~/helpers/functions', () => ({
+ isSshPubKeyDuplicate: vi.fn(),
+}));
+vi.mock('~/services');
+
+const mockRepoList: Repository[] = [
+ {
+ id: 1,
+ alias: 'repo1',
+ repositoryName: 'Test Repository 1',
+ status: true,
+ lastSave: 1678901234,
+ alert: 1,
+ storageSize: 100,
+ storageUsed: 50,
+ sshPublicKey: 'ssh-rsa AAAAB3Nza...fakekey1',
+ comment: 'Test repository 1',
+ displayDetails: true,
+ unixUser: 'user1',
+ lanCommand: false,
+ appendOnlyMode: false,
+ lastStatusAlertSend: 1678901234,
+ },
+ {
+ id: 2,
+ alias: 'repo2',
+ repositoryName: 'Test Repository 2',
+ status: false,
+ lastSave: 1678905678,
+ storageSize: 200,
+ storageUsed: 150,
+ sshPublicKey: 'ssh-rsa AAAAB3Nza...fakekey2',
+ comment: 'Test repository 2',
+ displayDetails: false,
+ unixUser: 'user2',
+ },
+];
+
+describe('Repository GET info', () => {
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.resetModules();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 405 if method is not handling', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ const { req, res } = createMocks({ method: 'DELETE' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if no session or authorization header is provided', async () => {
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 401 if API key is invalid', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue(undefined);
+ const { req, res } = createMocks({
+ method: 'GET',
+ headers: { authorization: 'Bearer INVALID_API_KEY' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 403 if API key does not have read permissions', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue({ read: false });
+ const { req, res } = createMocks({
+ method: 'GET',
+ headers: { authorization: 'Bearer API_KEY' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(403);
+ });
+
+ it('should return 200 and the repoList data if found', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue(mockRepoList);
+ const { req, res } = createMocks({ method: 'GET' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ repoList: mockRepoList });
+ });
+});
+
+describe('Add a new repository', () => {
+ beforeEach(() => {
+ vi.resetModules();
+ vi.resetAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ it('should return 405 if method is not handling', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ const { req, res } = createMocks({ method: 'DELETE' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(405);
+ });
+
+ it('should return 401 if no session or authorization header is provided', async () => {
+ const { req, res } = createMocks({ method: 'POST' });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 401 if API key is invalid', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue(undefined);
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer INVALID_API_KEY' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(401);
+ });
+
+ it('should return 403 if API key does not have create permissions', async () => {
+ vi.mocked(getServerSession).mockResolvedValue(null);
+ vi.mocked(AuthService.tokenController).mockResolvedValue({ create: false });
+ const { req, res } = createMocks({
+ method: 'POST',
+ headers: { authorization: 'Bearer API_KEY' },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(403);
+ });
+
+ it('should return 409 if SSH key is duplicated', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ { id: 1, sshPublicKey: 'duplicate-key' },
+ ]);
+ vi.mocked(isSshPubKeyDuplicate).mockReturnValue(true);
+ const { req, res } = createMocks({
+ method: 'POST',
+ body: { alias: 'repo1', sshPublicKey: 'duplicate-key', storageSize: 10 },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(409);
+ });
+
+ it('should return 500 if createRepoShell fails', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
+ vi.mocked(ShellService.createRepo).mockResolvedValue({ stderr: 'Error' });
+ const { req, res } = createMocks({
+ method: 'POST',
+ body: { alias: 'repo1', sshPublicKey: 'valid-key', storageSize: 10 },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(500);
+ });
+
+ it('should successfully create a repository with a session', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
+ vi.mocked(ShellService.createRepo).mockResolvedValue({ stdout: 'new-repo' });
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue(true);
+ const { req, res } = createMocks({
+ method: 'POST',
+ body: { alias: 'repo1', sshPublicKey: 'valid-key', storageSize: 10 },
+ });
+ await handler(req, res);
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ id: 0, repositoryName: 'new-repo' });
+ });
+
+ it('should add missing optional properties with default values and update repo list correctly', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
+ vi.mocked(ShellService.createRepo).mockResolvedValue({ stdout: 'new-repo' });
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue(true);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ body: { alias: 'repo1', sshPublicKey: 'valid-key', storageSize: 10 },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ id: 0, repositoryName: 'new-repo' });
+
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith(
+ [
+ {
+ id: 0,
+ alias: 'repo1',
+ repositoryName: 'new-repo',
+ status: false,
+ lastSave: 0,
+ lastStatusAlertSend: expect.any(Number),
+ alert: 0,
+ storageSize: 10,
+ storageUsed: 0,
+ sshPublicKey: 'valid-key',
+ comment: '',
+ lanCommand: false,
+ appendOnlyMode: false,
+ },
+ ],
+ true
+ );
+ });
+
+ it('should assign the correct ID based on existing repositories', async () => {
+ vi.mocked(getServerSession).mockResolvedValue({ user: { name: 'USER' } });
+
+ vi.mocked(ConfigService.getRepoList).mockResolvedValue([
+ {
+ id: 0,
+ alias: 'repo0',
+ sshPublicKey: 'key0',
+ storageSize: 10,
+ repositoryName: '',
+ status: false,
+ lastSave: 0,
+ storageUsed: 0,
+ comment: '',
+ },
+ {
+ id: 1,
+ alias: 'repo1',
+ sshPublicKey: 'key1',
+ storageSize: 20,
+ repositoryName: '',
+ status: false,
+ lastSave: 0,
+ storageUsed: 0,
+ comment: '',
+ },
+ {
+ id: 3,
+ alias: 'repo3',
+ sshPublicKey: 'key3',
+ storageSize: 30,
+ repositoryName: '',
+ status: false,
+ lastSave: 0,
+ storageUsed: 0,
+ comment: '',
+ },
+ ]);
+
+ vi.mocked(ShellService.createRepo).mockResolvedValue({ stdout: 'new-repo' });
+ vi.mocked(ConfigService.updateRepoList).mockResolvedValue(true);
+
+ const { req, res } = createMocks({
+ method: 'POST',
+ body: { alias: 'repo-new', sshPublicKey: 'new-key', storageSize: 50 },
+ });
+
+ await handler(req, res);
+
+ expect(res._getStatusCode()).toBe(200);
+ expect(res._getJSONData()).toEqual({ id: 4, repositoryName: 'new-repo' });
+
+ expect(ConfigService.updateRepoList).toHaveBeenCalledWith(
+ [
+ {
+ id: 0,
+ alias: 'repo0',
+ sshPublicKey: 'key0',
+ storageSize: 10,
+ repositoryName: '',
+ status: false,
+ lastSave: 0,
+ storageUsed: 0,
+ comment: '',
+ },
+ {
+ id: 1,
+ alias: 'repo1',
+ sshPublicKey: 'key1',
+ storageSize: 20,
+ repositoryName: '',
+ status: false,
+ lastSave: 0,
+ storageUsed: 0,
+ comment: '',
+ },
+ {
+ id: 3,
+ alias: 'repo3',
+ sshPublicKey: 'key3',
+ storageSize: 30,
+ repositoryName: '',
+ status: false,
+ lastSave: 0,
+ storageUsed: 0,
+ comment: '',
+ },
+ {
+ id: 4,
+ alias: 'repo-new',
+ repositoryName: 'new-repo',
+ status: false,
+ lastSave: 0,
+ lastStatusAlertSend: expect.any(Number),
+ alert: 0,
+ storageSize: 50,
+ storageUsed: 0,
+ sshPublicKey: 'new-key',
+ comment: '',
+ lanCommand: false,
+ appendOnlyMode: false,
+ },
+ ],
+ true
+ );
+ });
+});
diff --git a/pages/api/v1/repositories/index.ts b/pages/api/v1/repositories/index.ts
new file mode 100644
index 0000000..ece275c
--- /dev/null
+++ b/pages/api/v1/repositories/index.ts
@@ -0,0 +1,145 @@
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { NextApiRequest, NextApiResponse } from 'next';
+import { BorgWarehouseApiResponse, Repository } from '~/types';
+import ApiResponse from '~/helpers/functions/apiResponse';
+import { ConfigService, AuthService, ShellService } from '~/services';
+import { isSshPubKeyDuplicate } from '~/helpers/functions';
+import { getUnixTime } from 'date-fns';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse<
+ BorgWarehouseApiResponse | { repoList: Repository[] } | { id: number; repositoryName: string }
+ >
+) {
+ const session = await getServerSession(req, res, authOptions);
+ const { authorization } = req.headers;
+ if (!session && !authorization) {
+ return ApiResponse.unauthorized(res);
+ }
+
+ if (req.method == 'GET') {
+ try {
+ if (!session && authorization) {
+ const permissions = await AuthService.tokenController(req.headers);
+ if (!permissions) {
+ return ApiResponse.unauthorized(res, 'Invalid API key');
+ }
+ if (!permissions.read) {
+ return ApiResponse.forbidden(res, 'Insufficient permissions');
+ }
+ }
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+
+ try {
+ const repoList = await ConfigService.getRepoList();
+
+ return res.status(200).json({ repoList });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+ } else if (req.method == 'POST') {
+ try {
+ if (!session && authorization) {
+ const permissions = await AuthService.tokenController(req.headers);
+ if (!permissions) {
+ return ApiResponse.unauthorized(res, 'Invalid API key');
+ }
+ if (!permissions.create) {
+ return ApiResponse.forbidden(res, 'Insufficient permissions');
+ }
+ }
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+
+ try {
+ validatePOSTRequestBody(req);
+ } catch (error) {
+ if (error instanceof Error) {
+ return ApiResponse.badRequest(res, error.message);
+ }
+ return ApiResponse.badRequest(res, 'Invalid request data');
+ }
+
+ try {
+ const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } =
+ req.body;
+ const repoList = await ConfigService.getRepoList();
+
+ if (sshPublicKey && isSshPubKeyDuplicate(sshPublicKey, repoList)) {
+ return res.status(409).json({
+ status: 409,
+ message:
+ 'The SSH key is already used in another repository. Please use another key or delete the key from the other repository.',
+ });
+ }
+
+ const newRepo: Repository = {
+ id: repoList.length > 0 ? Math.max(...repoList.map((repo) => repo.id)) + 1 : 0,
+ alias: alias,
+ repositoryName: '',
+ status: false,
+ lastSave: 0,
+ lastStatusAlertSend: getUnixTime(new Date()),
+ alert: alert ?? 0,
+ storageSize: storageSize,
+ storageUsed: 0,
+ sshPublicKey: sshPublicKey,
+ comment: comment ?? '',
+ lanCommand: lanCommand ?? false,
+ appendOnlyMode: appendOnlyMode ?? false,
+ };
+
+ const { stdout } = await ShellService.createRepo(
+ newRepo.sshPublicKey,
+ newRepo.storageSize,
+ newRepo.appendOnlyMode ?? false
+ );
+ if (!stdout) {
+ return ApiResponse.serverError(res, 'Error creating repository');
+ }
+
+ newRepo.repositoryName = stdout.trim();
+ const updatedRepoList = [...repoList, newRepo];
+ await ConfigService.updateRepoList(updatedRepoList, true);
+
+ return res.status(200).json({ id: newRepo.id, repositoryName: newRepo.repositoryName });
+ } catch (error) {
+ console.log(error);
+ return ApiResponse.serverError(res, error as string);
+ }
+ } else {
+ return ApiResponse.methodNotAllowed(res);
+ }
+}
+
+const validatePOSTRequestBody = (req: NextApiRequest) => {
+ const { alias, sshPublicKey, storageSize, comment, alert, lanCommand, appendOnlyMode } = req.body;
+ // Required fields
+ if (!alias || typeof alias !== 'string') {
+ throw new Error('Alias must be a non-empty string');
+ }
+ if (!sshPublicKey || typeof sshPublicKey !== 'string') {
+ throw new Error('SSH Public Key must be a non-empty string');
+ }
+ if (typeof storageSize !== 'number' || storageSize <= 0 || !Number.isInteger(storageSize)) {
+ throw new Error('Storage Size must be a positive integer');
+ }
+ // Optional fields
+ if (comment != undefined && typeof comment !== 'string') {
+ throw new Error('Comment must be a string');
+ }
+ if (alert != undefined && typeof alert !== 'number') {
+ throw new Error('Alert must be a number');
+ }
+ if (lanCommand != undefined && typeof lanCommand !== 'boolean') {
+ throw new Error('Lan Command must be a boolean');
+ }
+ if (appendOnlyMode != undefined && typeof appendOnlyMode !== 'boolean') {
+ throw new Error('Append Only Mode must be a boolean');
+ }
+};
diff --git a/pages/api/v1/version/index.ts b/pages/api/v1/version/index.ts
new file mode 100644
index 0000000..50fa110
--- /dev/null
+++ b/pages/api/v1/version/index.ts
@@ -0,0 +1,14 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import ApiResponse from '~/helpers/functions/apiResponse';
+import packageInfo from '~/package.json';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== 'GET') {
+ return res.status(405);
+ }
+ try {
+ return res.status(200).json({ version: packageInfo.version });
+ } catch (error) {
+ return ApiResponse.serverError(res, error);
+ }
+}
diff --git a/pages/api/version/index.js b/pages/api/version/index.js
deleted file mode 100644
index f2b4123..0000000
--- a/pages/api/version/index.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import packageInfo from '../../../package.json';
-
-export default async function handler(req, res) {
- if (req.method === 'GET') {
- try {
- res.status(200).json({ version: packageInfo.version });
- return;
- } catch (error) {
- res.status(500).json({
- status: 500,
- message: 'API error, contact the administrator !',
- });
- return;
- }
- }
-}
diff --git a/pages/index.js b/pages/index.js
deleted file mode 100644
index 95de8fe..0000000
--- a/pages/index.js
+++ /dev/null
@@ -1,53 +0,0 @@
-//Lib
-import { authOptions } from '../pages/api/auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-import { useSession } from 'next-auth/react';
-import Head from 'next/head';
-
-//Lib
-import RepoList from '../Containers/RepoList/RepoList';
-
-export default function Index() {
- const { status } = useSession();
-
- return (
- <>
- {status === 'unauthenticated' || status === 'loading' ? null : (
- <>
-
- {/* */}
- Repositories - BorgWarehouse
-
-
- >
- )}
- >
- );
-}
-
-export async function getServerSideProps(context) {
- //Var
- const session = await getServerSession(
- context.req,
- context.res,
- authOptions
- );
-
- if (!session) {
- return {
- redirect: {
- destination: '/login',
- permanent: false,
- },
- };
- }
-
- return {
- props: {},
- };
-}
diff --git a/pages/index.tsx b/pages/index.tsx
new file mode 100644
index 0000000..6d826ae
--- /dev/null
+++ b/pages/index.tsx
@@ -0,0 +1,46 @@
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { useSession } from 'next-auth/react';
+import Head from 'next/head';
+import RepoList from '../Containers/RepoList/RepoList';
+import { GetServerSidePropsContext } from 'next';
+
+export default function Index() {
+ const { status } = useSession();
+
+ return (
+ <>
+ {status === 'unauthenticated' || status === 'loading' ? null : (
+ <>
+
+ {/* */}
+ Repositories - BorgWarehouse
+
+
+ >
+ )}
+ >
+ );
+}
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ if (!session) {
+ return {
+ redirect: {
+ destination: '/login',
+ permanent: false,
+ },
+ };
+ }
+
+ return {
+ props: {},
+ };
+}
diff --git a/pages/login.js b/pages/login.js
deleted file mode 100644
index c96338e..0000000
--- a/pages/login.js
+++ /dev/null
@@ -1,192 +0,0 @@
-//Lib
-import { useForm } from 'react-hook-form';
-import { signIn } from 'next-auth/react';
-import { useState } from 'react';
-import { SpinnerDotted } from 'spinners-react';
-import { useRouter } from 'next/router';
-import { authOptions } from './api/auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-//Components
-import Error from '../Components/UI/Error/Error';
-
-export default function Login() {
- //Var
- const {
- register,
- handleSubmit,
- formState: { errors },
- reset,
- } = useForm();
- const router = useRouter();
-
- //State
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState();
-
- //Functions
- const formSubmitHandler = async (data) => {
- setIsLoading(true);
- setError(null);
- const resultat = await signIn('credentials', {
- username: data.username,
- password: data.password,
- redirect: false,
- });
-
- setIsLoading(false);
-
- if (resultat.error) {
- reset();
- setError(resultat.error);
- setTimeout(() => setError(), 4000);
- } else {
- router.replace('/');
- }
- };
-
- return (
-
- );
-}
-
-export async function getServerSideProps(context) {
- //Var
- const session = await getServerSession(
- context.req,
- context.res,
- authOptions
- );
-
- //Here, if I am connected, I redirect to the home page.
- if (session) {
- return {
- redirect: {
- destination: '/',
- permanent: false,
- },
- };
- }
-
- return {
- props: { session },
- };
-}
diff --git a/pages/login.tsx b/pages/login.tsx
new file mode 100644
index 0000000..49c7764
--- /dev/null
+++ b/pages/login.tsx
@@ -0,0 +1,180 @@
+import { getServerSession } from 'next-auth/next';
+import { signIn, useSession } from 'next-auth/react';
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+import { useForm } from 'react-hook-form';
+import { useFormStatus } from '~/hooks';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+
+//Components
+import { GetServerSidePropsContext } from 'next';
+import Image from 'next/image';
+import { ToastOptions, toast } from 'react-toastify';
+import { useLoader } from '~/contexts/LoaderContext';
+
+type LoginForm = {
+ username: string;
+ password: string;
+};
+
+export default function Login() {
+ const { status } = useSession();
+ const { register, handleSubmit, reset, setFocus } = useForm();
+ const router = useRouter();
+ const toastOptions: ToastOptions = {
+ position: 'top-center',
+ autoClose: 5000,
+ theme: 'dark',
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ style: {
+ backgroundColor: '#212942',
+ fontSize: '1.1rem',
+ },
+ };
+
+ const { isLoading, setIsLoading, handleError, clearError } = useFormStatus();
+ const { start, stop } = useLoader();
+
+ useEffect(() => {
+ if (status === 'authenticated') {
+ router.replace('/');
+ }
+ }, [status, router]);
+
+ // Block the rendering of the component if the user is already connected or in the process of connecting.
+ if (status === 'loading' || status === 'authenticated') {
+ return;
+ }
+
+ //Functions
+ const formSubmitHandler = async (data: LoginForm) => {
+ start();
+ setIsLoading(true);
+ clearError();
+ const resultat = await signIn('credentials', {
+ username: data.username,
+ password: data.password,
+ redirect: false,
+ });
+
+ if (resultat?.error) {
+ stop();
+ setFocus('username');
+ reset();
+ toast.info('Incorrect credentials', toastOptions);
+ handleError(resultat.error);
+ } else {
+ stop();
+ setIsLoading(false);
+ router.replace('/');
+ }
+ };
+
+ return (
+
+ );
+}
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ //Here, if I am connected, I redirect to the home page.
+ if (session) {
+ return {
+ redirect: {
+ destination: '/',
+ permanent: false,
+ },
+ };
+ }
+
+ return {
+ props: { session },
+ };
+}
diff --git a/pages/manage-repo/add.js b/pages/manage-repo/add.js
deleted file mode 100644
index 8e2576b..0000000
--- a/pages/manage-repo/add.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import RepoList from '../../Containers/RepoList/RepoList';
-import { authOptions } from '../../pages/api/auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default function Add() {
- return ;
-}
-
-export async function getServerSideProps(context) {
- //Var
- const session = await getServerSession(
- context.req,
- context.res,
- authOptions
- );
-
- if (!session) {
- return {
- redirect: {
- destination: '/login',
- permanent: false,
- },
- };
- }
-
- return {
- props: {},
- };
-}
diff --git a/pages/manage-repo/add.tsx b/pages/manage-repo/add.tsx
new file mode 100644
index 0000000..9435261
--- /dev/null
+++ b/pages/manage-repo/add.tsx
@@ -0,0 +1,25 @@
+import { GetServerSidePropsContext } from 'next';
+import RepoList from '~/Containers/RepoList/RepoList';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+
+export default function Add() {
+ return ;
+}
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ if (!session) {
+ return {
+ redirect: {
+ destination: '/login',
+ permanent: false,
+ },
+ };
+ }
+
+ return {
+ props: {},
+ };
+}
diff --git a/pages/manage-repo/edit/[slug].js b/pages/manage-repo/edit/[slug].js
deleted file mode 100644
index 3a7496a..0000000
--- a/pages/manage-repo/edit/[slug].js
+++ /dev/null
@@ -1,29 +0,0 @@
-import RepoList from '../../../Containers/RepoList/RepoList';
-import { authOptions } from '../../../pages/api/auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default function Add() {
- return ;
-}
-
-export async function getServerSideProps(context) {
- //Var
- const session = await getServerSession(
- context.req,
- context.res,
- authOptions
- );
-
- if (!session) {
- return {
- redirect: {
- destination: '/login',
- permanent: false,
- },
- };
- }
-
- return {
- props: {},
- };
-}
diff --git a/pages/manage-repo/edit/[slug].tsx b/pages/manage-repo/edit/[slug].tsx
new file mode 100644
index 0000000..9435261
--- /dev/null
+++ b/pages/manage-repo/edit/[slug].tsx
@@ -0,0 +1,25 @@
+import { GetServerSidePropsContext } from 'next';
+import RepoList from '~/Containers/RepoList/RepoList';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+
+export default function Add() {
+ return ;
+}
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ if (!session) {
+ return {
+ redirect: {
+ destination: '/login',
+ permanent: false,
+ },
+ };
+ }
+
+ return {
+ props: {},
+ };
+}
diff --git a/pages/monitoring/index.js b/pages/monitoring/index.js
deleted file mode 100644
index bd4225c..0000000
--- a/pages/monitoring/index.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import Head from 'next/head';
-import { authOptions } from '../../pages/api/auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-//Components
-import StorageUsedChartBar from '../../Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar';
-
-export default function Monitoring() {
- return (
- <>
-
- Monitoring - BorgWarehouse
-
-
-
-
-
- ๐ Storage used
-
-
-
-
-
-
- >
- );
-}
-
-export async function getServerSideProps(context) {
- //Var
- const session = await getServerSession(
- context.req,
- context.res,
- authOptions
- );
-
- if (!session) {
- return {
- redirect: {
- destination: '/login',
- permanent: false,
- },
- };
- }
-
- return {
- props: {},
- };
-}
diff --git a/pages/monitoring/index.tsx b/pages/monitoring/index.tsx
new file mode 100644
index 0000000..216caf5
--- /dev/null
+++ b/pages/monitoring/index.tsx
@@ -0,0 +1,64 @@
+import Head from 'next/head';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { GetServerSidePropsContext } from 'next';
+
+//Components
+import StorageUsedChartBar from '~/Containers/Monitoring/StorageUsedChartBar/StorageUsedChartBar';
+
+export default function Monitoring() {
+ return (
+ <>
+
+ Monitoring - BorgWarehouse
+
+
+
+
+
+ ๐ Storage used
+
+
+
+
+
+
+ >
+ );
+}
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ if (!session) {
+ return {
+ redirect: {
+ destination: '/login',
+ permanent: false,
+ },
+ };
+ }
+
+ return {
+ props: {},
+ };
+}
diff --git a/pages/setup-wizard/[slug].js b/pages/setup-wizard/[slug].js
deleted file mode 100644
index fbcb229..0000000
--- a/pages/setup-wizard/[slug].js
+++ /dev/null
@@ -1,43 +0,0 @@
-//Lib
-import SetupWizard from '../../Containers/SetupWizard/SetupWizard';
-import { useRouter } from 'next/router';
-import Head from 'next/head';
-import { authOptions } from '../../pages/api/auth/[...nextauth]';
-import { getServerSession } from 'next-auth/next';
-
-export default function SetupWizardStep() {
- ////Var
- const router = useRouter();
- const step = router.query.slug;
-
- return (
- <>
-
- Setup Wizard - BorgWarehouse
-
-
- >
- );
-}
-
-export async function getServerSideProps(context) {
- //Var
- const session = await getServerSession(
- context.req,
- context.res,
- authOptions
- );
-
- if (!session) {
- return {
- redirect: {
- destination: '/login',
- permanent: false,
- },
- };
- }
-
- return {
- props: {},
- };
-}
diff --git a/pages/setup-wizard/[slug].tsx b/pages/setup-wizard/[slug].tsx
new file mode 100644
index 0000000..b8687bc
--- /dev/null
+++ b/pages/setup-wizard/[slug].tsx
@@ -0,0 +1,39 @@
+import SetupWizard from '../../Containers/SetupWizard/SetupWizard';
+import { useRouter } from 'next/router';
+import Head from 'next/head';
+import { authOptions } from '~/pages/api/auth/[...nextauth]';
+import { getServerSession } from 'next-auth/next';
+import { GetServerSidePropsContext } from 'next';
+
+export default function SetupWizardStep() {
+ ////Var
+ const router = useRouter();
+ const slug = router.query.slug;
+ const step = Array.isArray(slug) ? parseInt(slug[0], 10) : slug ? parseInt(slug, 10) : 1;
+
+ return (
+ <>
+
+ Setup Wizard - BorgWarehouse
+
+
+ >
+ );
+}
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ if (!session) {
+ return {
+ redirect: {
+ destination: '/login',
+ permanent: false,
+ },
+ };
+ }
+
+ return {
+ props: {},
+ };
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..e31486f
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,5857 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@tabler/icons-react':
+ specifier: ^3.37.1
+ version: 3.37.1(react@19.2.4)
+ async-mutex:
+ specifier: ^0.5.0
+ version: 0.5.0
+ bcryptjs:
+ specifier: ^3.0.3
+ version: 3.0.3
+ chart.js:
+ specifier: ^4.5.1
+ version: 4.5.1
+ date-fns:
+ specifier: ^4.1.0
+ version: 4.1.0
+ lowdb:
+ specifier: ^7.0.1
+ version: 7.0.1
+ next:
+ specifier: ^16.1.6
+ version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next-auth:
+ specifier: ^4.24.13
+ version: 4.24.13(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ nodemailer:
+ specifier: ^8.0.1
+ version: 8.0.1
+ nprogress:
+ specifier: ^0.2.0
+ version: 0.2.0
+ react:
+ specifier: ^19.2.4
+ version: 19.2.4
+ react-chartjs-2:
+ specifier: ^5.3.1
+ version: 5.3.1(chart.js@4.5.1)(react@19.2.4)
+ react-dom:
+ specifier: ^19.2.4
+ version: 19.2.4(react@19.2.4)
+ react-hook-form:
+ specifier: ^7.71.2
+ version: 7.71.2(react@19.2.4)
+ react-select:
+ specifier: ^5.10.2
+ version: 5.10.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react-toastify:
+ specifier: ^11.0.5
+ version: 11.0.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ swr:
+ specifier: ^2.4.1
+ version: 2.4.1(react@19.2.4)
+ use-media:
+ specifier: ^1.5.0
+ version: 1.5.0(react@19.2.4)
+ uuid:
+ specifier: ^13.0.0
+ version: 13.0.0
+ devDependencies:
+ '@commitlint/cli':
+ specifier: ^20.4.2
+ version: 20.4.2(@types/node@25.3.3)(typescript@5.9.3)
+ '@commitlint/config-conventional':
+ specifier: ^20.4.2
+ version: 20.4.2
+ '@types/node':
+ specifier: ^25.3.3
+ version: 25.3.3
+ '@types/nodemailer':
+ specifier: ^7.0.11
+ version: 7.0.11
+ '@types/nprogress':
+ specifier: ^0.2.3
+ version: 0.2.3
+ '@types/react':
+ specifier: ^19.2.14
+ version: 19.2.14
+ '@types/supertest':
+ specifier: ^7.2.0
+ version: 7.2.0
+ eslint:
+ specifier: ^9.39.3
+ version: 9.39.3(jiti@2.6.1)
+ eslint-config-next:
+ specifier: ^16.1.6
+ version: 16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ husky:
+ specifier: ^9.1.7
+ version: 9.1.7
+ node-mocks-http:
+ specifier: ^1.17.2
+ version: 1.17.2(@types/node@25.3.3)
+ prettier:
+ specifier: ^3.8.1
+ version: 3.8.1
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+ vitest:
+ specifier: ^4.0.18
+ version: 4.0.18(@types/node@25.3.3)(jiti@2.6.1)
+
+packages:
+
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.0':
+ resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.0':
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.28.6':
+ resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.0':
+ resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/runtime@7.28.6':
+ resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ engines: {node: '>=6.9.0'}
+
+ '@commitlint/cli@20.4.2':
+ resolution: {integrity: sha512-YjYSX2yj/WsVoxh9mNiymfFS2ADbg2EK4+1WAsMuckwKMCqJ5PDG0CJU/8GvmHWcv4VRB2V02KqSiecRksWqZQ==}
+ engines: {node: '>=v18'}
+ hasBin: true
+
+ '@commitlint/config-conventional@20.4.2':
+ resolution: {integrity: sha512-rwkTF55q7Q+6dpSKUmJoScV0f3EpDlWKw2UPzklkLS4o5krMN1tPWAVOgHRtyUTMneIapLeQwaCjn44Td6OzBQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/config-validator@20.4.0':
+ resolution: {integrity: sha512-zShmKTF+sqyNOfAE0vKcqnpvVpG0YX8F9G/ZIQHI2CoKyK+PSdladXMSns400aZ5/QZs+0fN75B//3Q5CHw++w==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/ensure@20.4.1':
+ resolution: {integrity: sha512-WLQqaFx1pBooiVvBrA1YfJNFqZF8wS/YGOtr5RzApDbV9tQ52qT5VkTsY65hFTnXhW8PcDfZLaknfJTmPejmlw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/execute-rule@20.0.0':
+ resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/format@20.4.0':
+ resolution: {integrity: sha512-i3ki3WR0rgolFVX6r64poBHXM1t8qlFel1G1eCBvVgntE3fCJitmzSvH5JD/KVJN/snz6TfaX2CLdON7+s4WVQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/is-ignored@20.4.1':
+ resolution: {integrity: sha512-In5EO4JR1lNsAv1oOBBO24V9ND1IqdAJDKZiEpdfjDl2HMasAcT7oA+5BKONv1pRoLG380DGPE2W2RIcUwdgLA==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/lint@20.4.2':
+ resolution: {integrity: sha512-buquzNRtFng6xjXvBU1abY/WPEEjCgUipNQrNmIWe8QuJ6LWLtei/LDBAzEe5ASm45+Q9L2Xi3/GVvlj50GAug==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/load@20.4.0':
+ resolution: {integrity: sha512-Dauup/GfjwffBXRJUdlX/YRKfSVXsXZLnINXKz0VZkXdKDcaEILAi9oflHGbfydonJnJAbXEbF3nXPm9rm3G6A==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/message@20.4.0':
+ resolution: {integrity: sha512-B5lGtvHgiLAIsK5nLINzVW0bN5hXv+EW35sKhYHE8F7V9Uz1fR4tx3wt7mobA5UNhZKUNgB/+ldVMQE6IHZRyA==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/parse@20.4.1':
+ resolution: {integrity: sha512-XNtZjeRcFuAfUnhYrCY02+mpxwY4OmnvD3ETbVPs25xJFFz1nRo/25nHj+5eM+zTeRFvWFwD4GXWU2JEtoK1/w==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/read@20.4.0':
+ resolution: {integrity: sha512-QfpFn6/I240ySEGv7YWqho4vxqtPpx40FS7kZZDjUJ+eHxu3azfhy7fFb5XzfTqVNp1hNoI3tEmiEPbDB44+cg==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/resolve-extends@20.4.0':
+ resolution: {integrity: sha512-ay1KM8q0t+/OnlpqXJ+7gEFQNlUtSU5Gxr8GEwnVf2TPN3+ywc5DzL3JCxmpucqxfHBTFwfRMXxPRRnR5Ki20g==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/rules@20.4.2':
+ resolution: {integrity: sha512-oz83pnp5Yq6uwwTAabuVQPNlPfeD2Y5ZjMb7Wx8FSUlu4sLYJjbBWt8031Z0osCFPfHzAwSYrjnfDFKtuSMdKg==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/to-lines@20.0.0':
+ resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/top-level@20.4.0':
+ resolution: {integrity: sha512-NDzq8Q6jmFaIIBC/GG6n1OQEaHdmaAAYdrZRlMgW6glYWGZ+IeuXmiymDvQNXPc82mVxq2KiE3RVpcs+1OeDeA==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/types@20.4.0':
+ resolution: {integrity: sha512-aO5l99BQJ0X34ft8b0h7QFkQlqxC6e7ZPVmBKz13xM9O8obDaM1Cld4sQlJDXXU/VFuUzQ30mVtHjVz74TuStw==}
+ engines: {node: '>=v18'}
+
+ '@emnapi/core@1.8.1':
+ resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
+
+ '@emnapi/runtime@1.8.1':
+ resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
+
+ '@emnapi/wasi-threads@1.1.0':
+ resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
+
+ '@emotion/babel-plugin@11.13.5':
+ resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
+
+ '@emotion/cache@11.14.0':
+ resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==}
+
+ '@emotion/hash@0.9.2':
+ resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
+
+ '@emotion/memoize@0.9.0':
+ resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==}
+
+ '@emotion/react@11.14.0':
+ resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: '>=16.8.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@emotion/serialize@1.3.3':
+ resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==}
+
+ '@emotion/sheet@1.4.0':
+ resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==}
+
+ '@emotion/unitless@0.10.0':
+ resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==}
+
+ '@emotion/use-insertion-effect-with-fallbacks@1.2.0':
+ resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==}
+ peerDependencies:
+ react: '>=16.8.0'
+
+ '@emotion/utils@1.4.2':
+ resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==}
+
+ '@emotion/weak-memoize@0.4.0':
+ resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==}
+
+ '@esbuild/aix-ppc64@0.27.3':
+ resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.27.3':
+ resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.27.3':
+ resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.27.3':
+ resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.27.3':
+ resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.27.3':
+ resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.27.3':
+ resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.27.3':
+ resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.27.3':
+ resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.27.3':
+ resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.27.3':
+ resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.27.3':
+ resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.27.3':
+ resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.27.3':
+ resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.27.3':
+ resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.27.3':
+ resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.27.3':
+ resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.27.3':
+ resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.27.3':
+ resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.27.3':
+ resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.27.3':
+ resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.27.3':
+ resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.27.3':
+ resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.27.3':
+ resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.27.3':
+ resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.27.3':
+ resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.9.1':
+ resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.21.1':
+ resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.4.2':
+ resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.17.0':
+ resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.4':
+ resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.39.3':
+ resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.7':
+ resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.4.1':
+ resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@floating-ui/core@1.7.4':
+ resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
+
+ '@floating-ui/dom@1.7.5':
+ resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==}
+
+ '@floating-ui/utils@0.2.10':
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+
+ '@humanfs/core@0.19.1':
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.7':
+ resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@img/colour@1.0.0':
+ resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
+ engines: {node: '>=18'}
+
+ '@img/sharp-darwin-arm64@0.34.5':
+ resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-darwin-x64@0.34.5':
+ resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-arm@1.2.4':
+ resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
+ cpu: [arm]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
+ resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
+ resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-s390x@1.2.4':
+ resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-x64@1.2.4':
+ resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+ resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-linux-arm64@0.34.5':
+ resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-linux-arm@0.34.5':
+ resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@img/sharp-linux-ppc64@0.34.5':
+ resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@img/sharp-linux-riscv64@0.34.5':
+ resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-linux-s390x@0.34.5':
+ resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@img/sharp-linux-x64@0.34.5':
+ resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-linuxmusl-arm64@0.34.5':
+ resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-wasm32@0.34.5':
+ resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [wasm32]
+
+ '@img/sharp-win32-arm64@0.34.5':
+ resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@img/sharp-win32-ia32@0.34.5':
+ resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@img/sharp-win32-x64@0.34.5':
+ resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@kurkle/color@0.3.4':
+ resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
+
+ '@napi-rs/wasm-runtime@0.2.12':
+ resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
+
+ '@next/env@16.1.6':
+ resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==}
+
+ '@next/eslint-plugin-next@16.1.6':
+ resolution: {integrity: sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==}
+
+ '@next/swc-darwin-arm64@16.1.6':
+ resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@next/swc-darwin-x64@16.1.6':
+ resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@next/swc-linux-arm64-gnu@16.1.6':
+ resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@next/swc-linux-arm64-musl@16.1.6':
+ resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@next/swc-linux-x64-gnu@16.1.6':
+ resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@next/swc-linux-x64-musl@16.1.6':
+ resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@next/swc-win32-arm64-msvc@16.1.6':
+ resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@next/swc-win32-x64-msvc@16.1.6':
+ resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@nolyfill/is-core-module@1.0.39':
+ resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
+ engines: {node: '>=12.4.0'}
+
+ '@panva/hkdf@1.2.1':
+ resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
+
+ '@rollup/rollup-android-arm-eabi@4.59.0':
+ resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.59.0':
+ resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.59.0':
+ resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.59.0':
+ resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.59.0':
+ resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.59.0':
+ resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.59.0':
+ resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.59.0':
+ resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.59.0':
+ resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.59.0':
+ resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-gnu@4.59.0':
+ resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-musl@4.59.0':
+ resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.59.0':
+ resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-musl@4.59.0':
+ resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.59.0':
+ resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.59.0':
+ resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.59.0':
+ resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.59.0':
+ resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.59.0':
+ resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-openbsd-x64@4.59.0':
+ resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.59.0':
+ resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.59.0':
+ resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.59.0':
+ resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.59.0':
+ resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.59.0':
+ resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rtsao/scc@1.1.0':
+ resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@swc/helpers@0.5.15':
+ resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+
+ '@tabler/icons-react@3.37.1':
+ resolution: {integrity: sha512-R7UE71Jji7i4Su56Y9zU1uYEBakUejuDJvyuYVmBuUoqp/x3Pn4cv2huarexR3P0GJ2eHg4rUj9l5zccqS6K/Q==}
+ peerDependencies:
+ react: '>= 16'
+
+ '@tabler/icons@3.37.1':
+ resolution: {integrity: sha512-neLCWkuyNHEPXCyYu6nbN4S3g/59BTa4qyITAugYVpq1YzYNDOZooW7/vRWH98ZItXAudxdKU8muFT7y1PqzuA==}
+
+ '@tybys/wasm-util@0.10.1':
+ resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/cookiejar@2.1.5':
+ resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/json5@0.0.29':
+ resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
+
+ '@types/methods@1.1.4':
+ resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
+
+ '@types/node@25.3.3':
+ resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==}
+
+ '@types/nodemailer@7.0.11':
+ resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==}
+
+ '@types/nprogress@0.2.3':
+ resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
+
+ '@types/parse-json@4.0.2':
+ resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
+
+ '@types/react-transition-group@4.4.12':
+ resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==}
+ peerDependencies:
+ '@types/react': '*'
+
+ '@types/react@19.2.14':
+ resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
+
+ '@types/superagent@8.1.9':
+ resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==}
+
+ '@types/supertest@7.2.0':
+ resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==}
+
+ '@typescript-eslint/eslint-plugin@8.56.1':
+ resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.56.1
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/parser@8.56.1':
+ resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/project-service@8.56.1':
+ resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/scope-manager@8.56.1':
+ resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.56.1':
+ resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/type-utils@8.56.1':
+ resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/types@8.56.1':
+ resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.56.1':
+ resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/utils@8.56.1':
+ resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/visitor-keys@8.56.1':
+ resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@unrs/resolver-binding-android-arm-eabi@1.11.1':
+ resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
+ cpu: [arm]
+ os: [android]
+
+ '@unrs/resolver-binding-android-arm64@1.11.1':
+ resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==}
+ cpu: [arm64]
+ os: [android]
+
+ '@unrs/resolver-binding-darwin-arm64@1.11.1':
+ resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@unrs/resolver-binding-darwin-x64@1.11.1':
+ resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@unrs/resolver-binding-freebsd-x64@1.11.1':
+ resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1':
+ resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1':
+ resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-arm64-gnu@1.11.1':
+ resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
+ resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
+ resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
+ resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
+ resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
+ resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
+ resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
+ cpu: [x64]
+ os: [linux]
+
+ '@unrs/resolver-binding-linux-x64-musl@1.11.1':
+ resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@unrs/resolver-binding-wasm32-wasi@1.11.1':
+ resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@unrs/resolver-binding-win32-arm64-msvc@1.11.1':
+ resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@unrs/resolver-binding-win32-ia32-msvc@1.11.1':
+ resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
+ resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==}
+ cpu: [x64]
+ os: [win32]
+
+ '@vitest/expect@4.0.18':
+ resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
+
+ '@vitest/mocker@4.0.18':
+ resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.0.18':
+ resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==}
+
+ '@vitest/runner@4.0.18':
+ resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==}
+
+ '@vitest/snapshot@4.0.18':
+ resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==}
+
+ '@vitest/spy@4.0.18':
+ resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==}
+
+ '@vitest/utils@4.0.18':
+ resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
+
+ accepts@1.3.8:
+ resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
+ engines: {node: '>= 0.6'}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.16.0:
+ resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.14.0:
+ resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
+
+ ajv@8.18.0:
+ resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ array-buffer-byte-length@1.0.2:
+ resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
+ engines: {node: '>= 0.4'}
+
+ array-ify@1.0.0:
+ resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
+
+ array-includes@3.1.9:
+ resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.findlast@1.2.5:
+ resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.findlastindex@1.2.6:
+ resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.flat@1.3.3:
+ resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.flatmap@1.3.3:
+ resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.tosorted@1.1.4:
+ resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==}
+ engines: {node: '>= 0.4'}
+
+ arraybuffer.prototype.slice@1.0.4:
+ resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
+ engines: {node: '>= 0.4'}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ ast-types-flow@0.0.8:
+ resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
+
+ async-function@1.0.0:
+ resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
+ engines: {node: '>= 0.4'}
+
+ async-mutex@0.5.0:
+ resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ available-typed-arrays@1.0.7:
+ resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+ engines: {node: '>= 0.4'}
+
+ axe-core@4.11.1:
+ resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==}
+ engines: {node: '>=4'}
+
+ axobject-query@4.1.0:
+ resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
+ engines: {node: '>= 0.4'}
+
+ babel-plugin-macros@3.1.0:
+ resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
+ engines: {node: '>=10', npm: '>=6'}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ baseline-browser-mapping@2.10.0:
+ resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ bcryptjs@3.0.3:
+ resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
+ hasBin: true
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+ brace-expansion@5.0.4:
+ resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
+ engines: {node: 18 || 20 || >=22}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.28.1:
+ resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bind@1.0.8:
+ resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001775:
+ resolution: {integrity: sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==}
+
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ chart.js@4.5.1:
+ resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
+ engines: {pnpm: '>=8'}
+
+ client-only@0.0.1:
+ resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ compare-func@2.0.0:
+ resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ content-disposition@0.5.4:
+ resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
+ engines: {node: '>= 0.6'}
+
+ conventional-changelog-angular@8.1.0:
+ resolution: {integrity: sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w==}
+ engines: {node: '>=18'}
+
+ conventional-changelog-conventionalcommits@9.1.0:
+ resolution: {integrity: sha512-MnbEysR8wWa8dAEvbj5xcBgJKQlX/m0lhS8DsyAAWDHdfs2faDJxTgzRYlRYpXSe7UiKrIIlB4TrBKU9q9DgkA==}
+ engines: {node: '>=18'}
+
+ conventional-commits-parser@6.2.1:
+ resolution: {integrity: sha512-20pyHgnO40rvfI0NGF/xiEoFMkXDtkF8FwHvk5BokoFoCuTQRI8vrNCNFWUOfuolKJMm1tPCHc8GgYEtr1XRNA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ convert-source-map@1.9.0:
+ resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
+ cosmiconfig-typescript-loader@6.2.0:
+ resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==}
+ engines: {node: '>=v18'}
+ peerDependencies:
+ '@types/node': '*'
+ cosmiconfig: '>=9'
+ typescript: '>=5'
+
+ cosmiconfig@7.1.0:
+ resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
+ engines: {node: '>=10'}
+
+ cosmiconfig@9.0.0:
+ resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ damerau-levenshtein@1.0.8:
+ resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
+
+ dargs@8.1.0:
+ resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==}
+ engines: {node: '>=12'}
+
+ data-view-buffer@1.0.2:
+ resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-length@1.0.2:
+ resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-offset@1.0.1:
+ resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
+ engines: {node: '>= 0.4'}
+
+ date-fns@4.1.0:
+ resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
+
+ debug@3.2.7:
+ resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
+ define-properties@1.2.1:
+ resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+ engines: {node: '>= 0.4'}
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ depd@1.1.2:
+ resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
+ engines: {node: '>= 0.6'}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ doctrine@2.1.0:
+ resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
+ engines: {node: '>=0.10.0'}
+
+ dom-helpers@5.2.1:
+ resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
+
+ dot-prop@5.3.0:
+ resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
+ engines: {node: '>=8'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ electron-to-chromium@1.5.302:
+ resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ error-ex@1.3.4:
+ resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
+ es-abstract@1.24.1:
+ resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==}
+ engines: {node: '>= 0.4'}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-iterator-helpers@1.2.2:
+ resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==}
+ engines: {node: '>= 0.4'}
+
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ es-shim-unscopables@1.1.0:
+ resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==}
+ engines: {node: '>= 0.4'}
+
+ es-to-primitive@1.3.0:
+ resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
+ engines: {node: '>= 0.4'}
+
+ esbuild@0.27.3:
+ resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-config-next@16.1.6:
+ resolution: {integrity: sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==}
+ peerDependencies:
+ eslint: '>=9.0.0'
+ typescript: '>=3.3.1'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ eslint-import-resolver-node@0.3.9:
+ resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
+
+ eslint-import-resolver-typescript@3.10.1:
+ resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ eslint: '*'
+ eslint-plugin-import: '*'
+ eslint-plugin-import-x: '*'
+ peerDependenciesMeta:
+ eslint-plugin-import:
+ optional: true
+ eslint-plugin-import-x:
+ optional: true
+
+ eslint-module-utils@2.12.1:
+ resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@typescript-eslint/parser': '*'
+ eslint: '*'
+ eslint-import-resolver-node: '*'
+ eslint-import-resolver-typescript: '*'
+ eslint-import-resolver-webpack: '*'
+ peerDependenciesMeta:
+ '@typescript-eslint/parser':
+ optional: true
+ eslint:
+ optional: true
+ eslint-import-resolver-node:
+ optional: true
+ eslint-import-resolver-typescript:
+ optional: true
+ eslint-import-resolver-webpack:
+ optional: true
+
+ eslint-plugin-import@2.32.0:
+ resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@typescript-eslint/parser': '*'
+ eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
+ peerDependenciesMeta:
+ '@typescript-eslint/parser':
+ optional: true
+
+ eslint-plugin-jsx-a11y@6.10.2:
+ resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
+
+ eslint-plugin-react-hooks@7.0.1:
+ resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+ eslint-plugin-react@7.37.5:
+ resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
+
+ eslint-scope@8.4.0:
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.1:
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint@9.39.3:
+ resolution: {integrity: sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.4.0:
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ expect-type@1.3.0:
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+ engines: {node: '>=12.0.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@3.3.1:
+ resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fast-uri@3.1.0:
+ resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
+
+ fastq@1.20.1:
+ resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ find-root@1.1.0:
+ resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.3.3:
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+ for-each@0.3.5:
+ resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
+ engines: {node: '>= 0.4'}
+
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+ engines: {node: '>= 6'}
+
+ fresh@0.5.2:
+ resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
+ engines: {node: '>= 0.6'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ function.prototype.name@1.1.8:
+ resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==}
+ engines: {node: '>= 0.4'}
+
+ functions-have-names@1.2.3:
+ resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+
+ generator-function@2.0.1:
+ resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
+ engines: {node: '>= 0.4'}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-symbol-description@1.1.0:
+ resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
+ engines: {node: '>= 0.4'}
+
+ get-tsconfig@4.13.6:
+ resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
+
+ git-raw-commits@4.0.0:
+ resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==}
+ engines: {node: '>=16'}
+ hasBin: true
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ global-directory@4.0.1:
+ resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
+ engines: {node: '>=18'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globals@16.4.0:
+ resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==}
+ engines: {node: '>=18'}
+
+ globalthis@1.0.4:
+ resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
+ engines: {node: '>= 0.4'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ has-bigints@1.1.0:
+ resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
+ engines: {node: '>= 0.4'}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+ has-proto@1.2.0:
+ resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==}
+ engines: {node: '>= 0.4'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ hermes-estree@0.25.1:
+ resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+
+ hermes-parser@0.25.1:
+ resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+
+ hoist-non-react-statics@3.3.2:
+ resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+
+ husky@9.1.7:
+ resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ import-meta-resolve@4.2.0:
+ resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ ini@4.1.1:
+ resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+ internal-slot@1.1.0:
+ resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
+ engines: {node: '>= 0.4'}
+
+ is-array-buffer@3.0.5:
+ resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
+ engines: {node: '>= 0.4'}
+
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
+ is-async-function@2.1.1:
+ resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
+ engines: {node: '>= 0.4'}
+
+ is-bigint@1.1.0:
+ resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
+ engines: {node: '>= 0.4'}
+
+ is-boolean-object@1.2.2:
+ resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
+ engines: {node: '>= 0.4'}
+
+ is-bun-module@2.0.0:
+ resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==}
+
+ is-callable@1.2.7:
+ resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+ engines: {node: '>= 0.4'}
+
+ is-core-module@2.16.1:
+ resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
+ engines: {node: '>= 0.4'}
+
+ is-data-view@1.0.2:
+ resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==}
+ engines: {node: '>= 0.4'}
+
+ is-date-object@1.1.0:
+ resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
+ engines: {node: '>= 0.4'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-finalizationregistry@1.1.1:
+ resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
+ engines: {node: '>= 0.4'}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-generator-function@1.1.2:
+ resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
+ engines: {node: '>= 0.4'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-map@2.0.3:
+ resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
+ engines: {node: '>= 0.4'}
+
+ is-negative-zero@2.0.3:
+ resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
+ engines: {node: '>= 0.4'}
+
+ is-number-object@1.1.1:
+ resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
+ engines: {node: '>= 0.4'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-obj@2.0.0:
+ resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
+ engines: {node: '>=8'}
+
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
+ is-regex@1.2.1:
+ resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+ engines: {node: '>= 0.4'}
+
+ is-set@2.0.3:
+ resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
+ engines: {node: '>= 0.4'}
+
+ is-shared-array-buffer@1.0.4:
+ resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
+ engines: {node: '>= 0.4'}
+
+ is-string@1.1.1:
+ resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
+ engines: {node: '>= 0.4'}
+
+ is-symbol@1.1.1:
+ resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
+ engines: {node: '>= 0.4'}
+
+ is-typed-array@1.1.15:
+ resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
+ engines: {node: '>= 0.4'}
+
+ is-weakmap@2.0.2:
+ resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
+ engines: {node: '>= 0.4'}
+
+ is-weakref@1.1.1:
+ resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==}
+ engines: {node: '>= 0.4'}
+
+ is-weakset@2.0.4:
+ resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
+ engines: {node: '>= 0.4'}
+
+ isarray@2.0.5:
+ resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ iterator.prototype@1.1.5:
+ resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
+ engines: {node: '>= 0.4'}
+
+ jiti@2.6.1:
+ resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+ hasBin: true
+
+ jose@4.15.9:
+ resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@1.0.2:
+ resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
+ hasBin: true
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ jsx-ast-utils@3.3.5:
+ resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
+ engines: {node: '>=4.0'}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ language-subtag-registry@0.3.23:
+ resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
+
+ language-tags@1.0.9:
+ resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
+ engines: {node: '>=0.10'}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.camelcase@4.3.0:
+ resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
+
+ lodash.kebabcase@4.1.1:
+ resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ lodash.mergewith@4.6.2:
+ resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
+
+ lodash.snakecase@4.1.1:
+ resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
+
+ lodash.startcase@4.4.0:
+ resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
+
+ lodash.upperfirst@4.3.1:
+ resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==}
+
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
+ lowdb@7.0.1:
+ resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==}
+ engines: {node: '>=18'}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ lru-cache@6.0.0:
+ resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
+ engines: {node: '>=10'}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ media-typer@0.3.0:
+ resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
+ engines: {node: '>= 0.6'}
+
+ memoize-one@6.0.0:
+ resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+
+ meow@12.1.1:
+ resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
+ engines: {node: '>=16.10'}
+
+ meow@13.2.0:
+ resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
+ engines: {node: '>=18'}
+
+ merge-descriptors@1.0.3:
+ resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ methods@1.1.2:
+ resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
+ engines: {node: '>= 0.6'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ mime@1.6.0:
+ resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ minimatch@10.2.4:
+ resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
+ engines: {node: 18 || 20 || >=22}
+
+ minimatch@3.1.5:
+ resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ napi-postinstall@0.3.4:
+ resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
+ engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ negotiator@0.6.3:
+ resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
+ engines: {node: '>= 0.6'}
+
+ next-auth@4.24.13:
+ resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==}
+ peerDependencies:
+ '@auth/core': 0.34.3
+ next: ^12.2.5 || ^13 || ^14 || ^15 || ^16
+ nodemailer: ^7.0.7
+ react: ^17.0.2 || ^18 || ^19
+ react-dom: ^17.0.2 || ^18 || ^19
+ peerDependenciesMeta:
+ '@auth/core':
+ optional: true
+ nodemailer:
+ optional: true
+
+ next@16.1.6:
+ resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==}
+ engines: {node: '>=20.9.0'}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ '@playwright/test': ^1.51.1
+ babel-plugin-react-compiler: '*'
+ react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ '@playwright/test':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+ sass:
+ optional: true
+
+ node-exports-info@1.6.0:
+ resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==}
+ engines: {node: '>= 0.4'}
+
+ node-mocks-http@1.17.2:
+ resolution: {integrity: sha512-HVxSnjNzE9NzoWMx9T9z4MLqwMpLwVvA0oVZ+L+gXskYXEJ6tFn3Kx4LargoB6ie7ZlCLplv7QbWO6N+MysWGA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@types/express': ^4.17.21 || ^5.0.0
+ '@types/node': '*'
+ peerDependenciesMeta:
+ '@types/express':
+ optional: true
+ '@types/node':
+ optional: true
+
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
+ nodemailer@8.0.1:
+ resolution: {integrity: sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==}
+ engines: {node: '>=6.0.0'}
+
+ nprogress@0.2.0:
+ resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
+
+ oauth@0.9.15:
+ resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
+
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-hash@2.2.0:
+ resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
+ engines: {node: '>= 6'}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ object-keys@1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
+
+ object.assign@4.1.7:
+ resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
+ engines: {node: '>= 0.4'}
+
+ object.entries@1.1.9:
+ resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==}
+ engines: {node: '>= 0.4'}
+
+ object.fromentries@2.0.8:
+ resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==}
+ engines: {node: '>= 0.4'}
+
+ object.groupby@1.0.3:
+ resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==}
+ engines: {node: '>= 0.4'}
+
+ object.values@1.2.1:
+ resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
+ engines: {node: '>= 0.4'}
+
+ obug@2.1.1:
+ resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+
+ oidc-token-hash@5.2.0:
+ resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==}
+ engines: {node: ^10.13.0 || >=12.0.0}
+
+ openid-client@5.7.1:
+ resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ own-keys@1.0.1:
+ resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
+ engines: {node: '>= 0.4'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
+ parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+ path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ possible-typed-array-names@1.1.0:
+ resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
+ engines: {node: '>= 0.4'}
+
+ postcss@8.4.31:
+ resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ preact-render-to-string@5.2.6:
+ resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
+ peerDependencies:
+ preact: '>=10'
+
+ preact@10.28.4:
+ resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ prettier@3.8.1:
+ resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ pretty-format@3.8.0:
+ resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
+
+ prop-types@15.8.1:
+ resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+
+ react-chartjs-2@5.3.1:
+ resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==}
+ peerDependencies:
+ chart.js: ^4.1.1
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ react-dom@19.2.4:
+ resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
+ peerDependencies:
+ react: ^19.2.4
+
+ react-hook-form@7.71.2:
+ resolution: {integrity: sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
+ react-is@16.13.1:
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
+ react-select@5.10.2:
+ resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ react-toastify@11.0.5:
+ resolution: {integrity: sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==}
+ peerDependencies:
+ react: ^18 || ^19
+ react-dom: ^18 || ^19
+
+ react-transition-group@4.4.5:
+ resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
+ peerDependencies:
+ react: '>=16.6.0'
+ react-dom: '>=16.6.0'
+
+ react@19.2.4:
+ resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
+ engines: {node: '>=0.10.0'}
+
+ reflect.getprototypeof@1.0.10:
+ resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
+ engines: {node: '>= 0.4'}
+
+ regexp.prototype.flags@1.5.4:
+ resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
+ engines: {node: '>= 0.4'}
+
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve-from@5.0.0:
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+ engines: {node: '>=8'}
+
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+ resolve@1.22.11:
+ resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
+ resolve@2.0.0-next.6:
+ resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rollup@4.59.0:
+ resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ safe-array-concat@1.1.3:
+ resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
+ engines: {node: '>=0.4'}
+
+ safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+ safe-push-apply@1.0.0:
+ resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
+ engines: {node: '>= 0.4'}
+
+ safe-regex-test@1.1.0:
+ resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+ engines: {node: '>= 0.4'}
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ set-function-name@2.0.2:
+ resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+ engines: {node: '>= 0.4'}
+
+ set-proto@1.0.0:
+ resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
+ engines: {node: '>= 0.4'}
+
+ sharp@0.34.5:
+ resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.5.7:
+ resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
+ engines: {node: '>=0.10.0'}
+
+ split2@4.2.0:
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+ engines: {node: '>= 10.x'}
+
+ stable-hash@0.0.5:
+ resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
+ steno@4.0.2:
+ resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==}
+ engines: {node: '>=18'}
+
+ stop-iteration-iterator@1.1.0:
+ resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
+ engines: {node: '>= 0.4'}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string.prototype.includes@2.0.1:
+ resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.matchall@4.0.12:
+ resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.repeat@1.0.0:
+ resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==}
+
+ string.prototype.trim@1.2.10:
+ resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimend@1.0.9:
+ resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimstart@1.0.8:
+ resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
+ engines: {node: '>= 0.4'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-bom@3.0.0:
+ resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+ engines: {node: '>=4'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ styled-jsx@5.1.6:
+ resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
+ engines: {node: '>= 12.0.0'}
+ peerDependencies:
+ '@babel/core': '*'
+ babel-plugin-macros: '*'
+ react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ babel-plugin-macros:
+ optional: true
+
+ stylis@4.2.0:
+ resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
+ swr@2.4.1:
+ resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==}
+ peerDependencies:
+ react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@1.0.2:
+ resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ tinyrainbow@3.0.3:
+ resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
+ engines: {node: '>=14.0.0'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ ts-api-utils@2.4.0:
+ resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ tsconfig-paths@3.15.0:
+ resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ type-is@1.6.18:
+ resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
+ engines: {node: '>= 0.6'}
+
+ typed-array-buffer@1.0.3:
+ resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-length@1.0.3:
+ resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-offset@1.0.4:
+ resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-length@1.0.7:
+ resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
+ engines: {node: '>= 0.4'}
+
+ typescript-eslint@8.56.1:
+ resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ unbox-primitive@1.1.0:
+ resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
+ engines: {node: '>= 0.4'}
+
+ undici-types@7.18.2:
+ resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+
+ unrs-resolver@1.11.1:
+ resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ use-isomorphic-layout-effect@1.2.1:
+ resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-media@1.5.0:
+ resolution: {integrity: sha512-gEKsAwpqeejjLXllfQLlktok4CX3cpsS0TChDR9VTl3xQ/231jWujjebnJrt+IH50RRO163JPvHVKBj+W+Weqg==}
+ peerDependencies:
+ react: '*'
+
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ uuid@13.0.0:
+ resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
+ hasBin: true
+
+ uuid@8.3.2:
+ resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+ hasBin: true
+
+ vite@7.3.1:
+ resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitest@4.0.18:
+ resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.0.18
+ '@vitest/browser-preview': 4.0.18
+ '@vitest/browser-webdriverio': 4.0.18
+ '@vitest/ui': 4.0.18
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ which-boxed-primitive@1.1.1:
+ resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
+ engines: {node: '>= 0.4'}
+
+ which-builtin-type@1.2.1:
+ resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==}
+ engines: {node: '>= 0.4'}
+
+ which-collection@1.0.2:
+ resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
+ engines: {node: '>= 0.4'}
+
+ which-typed-array@1.1.20:
+ resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
+ engines: {node: '>= 0.4'}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yallist@4.0.0:
+ resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+
+ yaml@1.10.2:
+ resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
+ engines: {node: '>= 6'}
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zod-validation-error@4.0.2:
+ resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ zod: ^3.25.0 || ^4.0.0
+
+ zod@4.3.6:
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+
+snapshots:
+
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.0': {}
+
+ '@babel/core@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helpers': 7.28.6
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.1':
+ dependencies:
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.28.6':
+ dependencies:
+ '@babel/compat-data': 7.29.0
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.1
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-module-imports@7.28.6':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.28.6':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+
+ '@babel/parser@7.29.0':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/runtime@7.28.6': {}
+
+ '@babel/template@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+
+ '@babel/traverse@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@commitlint/cli@20.4.2(@types/node@25.3.3)(typescript@5.9.3)':
+ dependencies:
+ '@commitlint/format': 20.4.0
+ '@commitlint/lint': 20.4.2
+ '@commitlint/load': 20.4.0(@types/node@25.3.3)(typescript@5.9.3)
+ '@commitlint/read': 20.4.0
+ '@commitlint/types': 20.4.0
+ tinyexec: 1.0.2
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - '@types/node'
+ - typescript
+
+ '@commitlint/config-conventional@20.4.2':
+ dependencies:
+ '@commitlint/types': 20.4.0
+ conventional-changelog-conventionalcommits: 9.1.0
+
+ '@commitlint/config-validator@20.4.0':
+ dependencies:
+ '@commitlint/types': 20.4.0
+ ajv: 8.18.0
+
+ '@commitlint/ensure@20.4.1':
+ dependencies:
+ '@commitlint/types': 20.4.0
+ lodash.camelcase: 4.3.0
+ lodash.kebabcase: 4.1.1
+ lodash.snakecase: 4.1.1
+ lodash.startcase: 4.4.0
+ lodash.upperfirst: 4.3.1
+
+ '@commitlint/execute-rule@20.0.0': {}
+
+ '@commitlint/format@20.4.0':
+ dependencies:
+ '@commitlint/types': 20.4.0
+ picocolors: 1.1.1
+
+ '@commitlint/is-ignored@20.4.1':
+ dependencies:
+ '@commitlint/types': 20.4.0
+ semver: 7.7.4
+
+ '@commitlint/lint@20.4.2':
+ dependencies:
+ '@commitlint/is-ignored': 20.4.1
+ '@commitlint/parse': 20.4.1
+ '@commitlint/rules': 20.4.2
+ '@commitlint/types': 20.4.0
+
+ '@commitlint/load@20.4.0(@types/node@25.3.3)(typescript@5.9.3)':
+ dependencies:
+ '@commitlint/config-validator': 20.4.0
+ '@commitlint/execute-rule': 20.0.0
+ '@commitlint/resolve-extends': 20.4.0
+ '@commitlint/types': 20.4.0
+ cosmiconfig: 9.0.0(typescript@5.9.3)
+ cosmiconfig-typescript-loader: 6.2.0(@types/node@25.3.3)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3)
+ is-plain-obj: 4.1.0
+ lodash.mergewith: 4.6.2
+ picocolors: 1.1.1
+ transitivePeerDependencies:
+ - '@types/node'
+ - typescript
+
+ '@commitlint/message@20.4.0': {}
+
+ '@commitlint/parse@20.4.1':
+ dependencies:
+ '@commitlint/types': 20.4.0
+ conventional-changelog-angular: 8.1.0
+ conventional-commits-parser: 6.2.1
+
+ '@commitlint/read@20.4.0':
+ dependencies:
+ '@commitlint/top-level': 20.4.0
+ '@commitlint/types': 20.4.0
+ git-raw-commits: 4.0.0
+ minimist: 1.2.8
+ tinyexec: 1.0.2
+
+ '@commitlint/resolve-extends@20.4.0':
+ dependencies:
+ '@commitlint/config-validator': 20.4.0
+ '@commitlint/types': 20.4.0
+ global-directory: 4.0.1
+ import-meta-resolve: 4.2.0
+ lodash.mergewith: 4.6.2
+ resolve-from: 5.0.0
+
+ '@commitlint/rules@20.4.2':
+ dependencies:
+ '@commitlint/ensure': 20.4.1
+ '@commitlint/message': 20.4.0
+ '@commitlint/to-lines': 20.0.0
+ '@commitlint/types': 20.4.0
+
+ '@commitlint/to-lines@20.0.0': {}
+
+ '@commitlint/top-level@20.4.0':
+ dependencies:
+ escalade: 3.2.0
+
+ '@commitlint/types@20.4.0':
+ dependencies:
+ conventional-commits-parser: 6.2.1
+ picocolors: 1.1.1
+
+ '@emnapi/core@1.8.1':
+ dependencies:
+ '@emnapi/wasi-threads': 1.1.0
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.8.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.1.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emotion/babel-plugin@11.13.5':
+ dependencies:
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/runtime': 7.28.6
+ '@emotion/hash': 0.9.2
+ '@emotion/memoize': 0.9.0
+ '@emotion/serialize': 1.3.3
+ babel-plugin-macros: 3.1.0
+ convert-source-map: 1.9.0
+ escape-string-regexp: 4.0.0
+ find-root: 1.1.0
+ source-map: 0.5.7
+ stylis: 4.2.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@emotion/cache@11.14.0':
+ dependencies:
+ '@emotion/memoize': 0.9.0
+ '@emotion/sheet': 1.4.0
+ '@emotion/utils': 1.4.2
+ '@emotion/weak-memoize': 0.4.0
+ stylis: 4.2.0
+
+ '@emotion/hash@0.9.2': {}
+
+ '@emotion/memoize@0.9.0': {}
+
+ '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@babel/runtime': 7.28.6
+ '@emotion/babel-plugin': 11.13.5
+ '@emotion/cache': 11.14.0
+ '@emotion/serialize': 1.3.3
+ '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4)
+ '@emotion/utils': 1.4.2
+ '@emotion/weak-memoize': 0.4.0
+ hoist-non-react-statics: 3.3.2
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+ transitivePeerDependencies:
+ - supports-color
+
+ '@emotion/serialize@1.3.3':
+ dependencies:
+ '@emotion/hash': 0.9.2
+ '@emotion/memoize': 0.9.0
+ '@emotion/unitless': 0.10.0
+ '@emotion/utils': 1.4.2
+ csstype: 3.2.3
+
+ '@emotion/sheet@1.4.0': {}
+
+ '@emotion/unitless@0.10.0': {}
+
+ '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+
+ '@emotion/utils@1.4.2': {}
+
+ '@emotion/weak-memoize@0.4.0': {}
+
+ '@esbuild/aix-ppc64@0.27.3':
+ optional: true
+
+ '@esbuild/android-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/android-arm@0.27.3':
+ optional: true
+
+ '@esbuild/android-x64@0.27.3':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/darwin-x64@0.27.3':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-arm@0.27.3':
+ optional: true
+
+ '@esbuild/linux-ia32@0.27.3':
+ optional: true
+
+ '@esbuild/linux-loong64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.27.3':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-s390x@0.27.3':
+ optional: true
+
+ '@esbuild/linux-x64@0.27.3':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.27.3':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.27.3':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/sunos-x64@0.27.3':
+ optional: true
+
+ '@esbuild/win32-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/win32-ia32@0.27.3':
+ optional: true
+
+ '@esbuild/win32-x64@0.27.3':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@2.6.1))':
+ dependencies:
+ eslint: 9.39.3(jiti@2.6.1)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.2': {}
+
+ '@eslint/config-array@0.21.1':
+ dependencies:
+ '@eslint/object-schema': 2.1.7
+ debug: 4.4.3
+ minimatch: 3.1.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.4.2':
+ dependencies:
+ '@eslint/core': 0.17.0
+
+ '@eslint/core@0.17.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.4':
+ dependencies:
+ ajv: 6.14.0
+ debug: 4.4.3
+ espree: 10.4.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ minimatch: 3.1.5
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.39.3': {}
+
+ '@eslint/object-schema@2.1.7': {}
+
+ '@eslint/plugin-kit@0.4.1':
+ dependencies:
+ '@eslint/core': 0.17.0
+ levn: 0.4.1
+
+ '@floating-ui/core@1.7.4':
+ dependencies:
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/dom@1.7.5':
+ dependencies:
+ '@floating-ui/core': 1.7.4
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/utils@0.2.10': {}
+
+ '@humanfs/core@0.19.1': {}
+
+ '@humanfs/node@0.16.7':
+ dependencies:
+ '@humanfs/core': 0.19.1
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@img/colour@1.0.0':
+ optional: true
+
+ '@img/sharp-darwin-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-darwin-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-s390x@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-arm@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-ppc64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-riscv64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-s390x@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-linuxmusl-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-wasm32@0.34.5':
+ dependencies:
+ '@emnapi/runtime': 1.8.1
+ optional: true
+
+ '@img/sharp-win32-arm64@0.34.5':
+ optional: true
+
+ '@img/sharp-win32-ia32@0.34.5':
+ optional: true
+
+ '@img/sharp-win32-x64@0.34.5':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@kurkle/color@0.3.4': {}
+
+ '@napi-rs/wasm-runtime@0.2.12':
+ dependencies:
+ '@emnapi/core': 1.8.1
+ '@emnapi/runtime': 1.8.1
+ '@tybys/wasm-util': 0.10.1
+ optional: true
+
+ '@next/env@16.1.6': {}
+
+ '@next/eslint-plugin-next@16.1.6':
+ dependencies:
+ fast-glob: 3.3.1
+
+ '@next/swc-darwin-arm64@16.1.6':
+ optional: true
+
+ '@next/swc-darwin-x64@16.1.6':
+ optional: true
+
+ '@next/swc-linux-arm64-gnu@16.1.6':
+ optional: true
+
+ '@next/swc-linux-arm64-musl@16.1.6':
+ optional: true
+
+ '@next/swc-linux-x64-gnu@16.1.6':
+ optional: true
+
+ '@next/swc-linux-x64-musl@16.1.6':
+ optional: true
+
+ '@next/swc-win32-arm64-msvc@16.1.6':
+ optional: true
+
+ '@next/swc-win32-x64-msvc@16.1.6':
+ optional: true
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.20.1
+
+ '@nolyfill/is-core-module@1.0.39': {}
+
+ '@panva/hkdf@1.2.1': {}
+
+ '@rollup/rollup-android-arm-eabi@4.59.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.59.0':
+ optional: true
+
+ '@rtsao/scc@1.1.0': {}
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@swc/helpers@0.5.15':
+ dependencies:
+ tslib: 2.8.1
+
+ '@tabler/icons-react@3.37.1(react@19.2.4)':
+ dependencies:
+ '@tabler/icons': 3.37.1
+ react: 19.2.4
+
+ '@tabler/icons@3.37.1': {}
+
+ '@tybys/wasm-util@0.10.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/cookiejar@2.1.5': {}
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/estree@1.0.8': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/json5@0.0.29': {}
+
+ '@types/methods@1.1.4': {}
+
+ '@types/node@25.3.3':
+ dependencies:
+ undici-types: 7.18.2
+
+ '@types/nodemailer@7.0.11':
+ dependencies:
+ '@types/node': 25.3.3
+
+ '@types/nprogress@0.2.3': {}
+
+ '@types/parse-json@4.0.2': {}
+
+ '@types/react-transition-group@4.4.12(@types/react@19.2.14)':
+ dependencies:
+ '@types/react': 19.2.14
+
+ '@types/react@19.2.14':
+ dependencies:
+ csstype: 3.2.3
+
+ '@types/superagent@8.1.9':
+ dependencies:
+ '@types/cookiejar': 2.1.5
+ '@types/methods': 1.1.4
+ '@types/node': 25.3.3
+ form-data: 4.0.5
+
+ '@types/supertest@7.2.0':
+ dependencies:
+ '@types/methods': 1.1.4
+ '@types/superagent': 8.1.9
+
+ '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.56.1
+ '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.56.1
+ eslint: 9.39.3(jiti@2.6.1)
+ ignore: 7.0.5
+ natural-compare: 1.4.0
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.56.1
+ '@typescript-eslint/types': 8.56.1
+ '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.56.1
+ debug: 4.4.3
+ eslint: 9.39.3(jiti@2.6.1)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.56.1
+ debug: 4.4.3
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.56.1':
+ dependencies:
+ '@typescript-eslint/types': 8.56.1
+ '@typescript-eslint/visitor-keys': 8.56.1
+
+ '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)':
+ dependencies:
+ typescript: 5.9.3
+
+ '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.56.1
+ '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ debug: 4.4.3
+ eslint: 9.39.3(jiti@2.6.1)
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.56.1': {}
+
+ '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.56.1
+ '@typescript-eslint/visitor-keys': 8.56.1
+ debug: 4.4.3
+ minimatch: 10.2.4
+ semver: 7.7.4
+ tinyglobby: 0.2.15
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1))
+ '@typescript-eslint/scope-manager': 8.56.1
+ '@typescript-eslint/types': 8.56.1
+ '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
+ eslint: 9.39.3(jiti@2.6.1)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.56.1':
+ dependencies:
+ '@typescript-eslint/types': 8.56.1
+ eslint-visitor-keys: 5.0.1
+
+ '@unrs/resolver-binding-android-arm-eabi@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-android-arm64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-darwin-arm64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-darwin-x64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-freebsd-x64@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-linux-x64-musl@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-wasm32-wasi@1.11.1':
+ dependencies:
+ '@napi-rs/wasm-runtime': 0.2.12
+ optional: true
+
+ '@unrs/resolver-binding-win32-arm64-msvc@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-win32-ia32-msvc@1.11.1':
+ optional: true
+
+ '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
+ optional: true
+
+ '@vitest/expect@4.0.18':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.0.18
+ '@vitest/utils': 4.0.18
+ chai: 6.2.2
+ tinyrainbow: 3.0.3
+
+ '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1))':
+ dependencies:
+ '@vitest/spy': 4.0.18
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)
+
+ '@vitest/pretty-format@4.0.18':
+ dependencies:
+ tinyrainbow: 3.0.3
+
+ '@vitest/runner@4.0.18':
+ dependencies:
+ '@vitest/utils': 4.0.18
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.0.18':
+ dependencies:
+ '@vitest/pretty-format': 4.0.18
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.0.18': {}
+
+ '@vitest/utils@4.0.18':
+ dependencies:
+ '@vitest/pretty-format': 4.0.18
+ tinyrainbow: 3.0.3
+
+ accepts@1.3.8:
+ dependencies:
+ mime-types: 2.1.35
+ negotiator: 0.6.3
+
+ acorn-jsx@5.3.2(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+
+ acorn@8.16.0: {}
+
+ ajv@6.14.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ajv@8.18.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.0
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
+ aria-query@5.3.2: {}
+
+ array-buffer-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ is-array-buffer: 3.0.5
+
+ array-ify@1.0.0: {}
+
+ array-includes@3.1.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ is-string: 1.1.1
+ math-intrinsics: 1.1.0
+
+ array.prototype.findlast@1.2.5:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.findlastindex@1.2.6:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.flat@1.3.3:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.flatmap@1.3.3:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.tosorted@1.1.4:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-shim-unscopables: 1.1.0
+
+ arraybuffer.prototype.slice@1.0.4:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ is-array-buffer: 3.0.5
+
+ assertion-error@2.0.1: {}
+
+ ast-types-flow@0.0.8: {}
+
+ async-function@1.0.0: {}
+
+ async-mutex@0.5.0:
+ dependencies:
+ tslib: 2.8.1
+
+ asynckit@0.4.0: {}
+
+ available-typed-arrays@1.0.7:
+ dependencies:
+ possible-typed-array-names: 1.1.0
+
+ axe-core@4.11.1: {}
+
+ axobject-query@4.1.0: {}
+
+ babel-plugin-macros@3.1.0:
+ dependencies:
+ '@babel/runtime': 7.28.6
+ cosmiconfig: 7.1.0
+ resolve: 1.22.11
+
+ balanced-match@1.0.2: {}
+
+ balanced-match@4.0.4: {}
+
+ baseline-browser-mapping@2.10.0: {}
+
+ bcryptjs@3.0.3: {}
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@5.0.4:
+ dependencies:
+ balanced-match: 4.0.4
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist@4.28.1:
+ dependencies:
+ baseline-browser-mapping: 2.10.0
+ caniuse-lite: 1.0.30001775
+ electron-to-chromium: 1.5.302
+ node-releases: 2.0.27
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bind@1.0.8:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ get-intrinsic: 1.3.0
+ set-function-length: 1.2.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001775: {}
+
+ chai@6.2.2: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ chart.js@4.5.1:
+ dependencies:
+ '@kurkle/color': 0.3.4
+
+ client-only@0.0.1: {}
+
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
+ clsx@2.1.1: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ compare-func@2.0.0:
+ dependencies:
+ array-ify: 1.0.0
+ dot-prop: 5.3.0
+
+ concat-map@0.0.1: {}
+
+ content-disposition@0.5.4:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ conventional-changelog-angular@8.1.0:
+ dependencies:
+ compare-func: 2.0.0
+
+ conventional-changelog-conventionalcommits@9.1.0:
+ dependencies:
+ compare-func: 2.0.0
+
+ conventional-commits-parser@6.2.1:
+ dependencies:
+ meow: 13.2.0
+
+ convert-source-map@1.9.0: {}
+
+ convert-source-map@2.0.0: {}
+
+ cookie@0.7.2: {}
+
+ cosmiconfig-typescript-loader@6.2.0(@types/node@25.3.3)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3):
+ dependencies:
+ '@types/node': 25.3.3
+ cosmiconfig: 9.0.0(typescript@5.9.3)
+ jiti: 2.6.1
+ typescript: 5.9.3
+
+ cosmiconfig@7.1.0:
+ dependencies:
+ '@types/parse-json': 4.0.2
+ import-fresh: 3.3.1
+ parse-json: 5.2.0
+ path-type: 4.0.0
+ yaml: 1.10.2
+
+ cosmiconfig@9.0.0(typescript@5.9.3):
+ dependencies:
+ env-paths: 2.2.1
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ parse-json: 5.2.0
+ optionalDependencies:
+ typescript: 5.9.3
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.2.3: {}
+
+ damerau-levenshtein@1.0.8: {}
+
+ dargs@8.1.0: {}
+
+ data-view-buffer@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-offset@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ date-fns@4.1.0: {}
+
+ debug@3.2.7:
+ dependencies:
+ ms: 2.1.3
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ define-properties@1.2.1:
+ dependencies:
+ define-data-property: 1.1.4
+ has-property-descriptors: 1.0.2
+ object-keys: 1.1.1
+
+ delayed-stream@1.0.0: {}
+
+ depd@1.1.2: {}
+
+ dequal@2.0.3: {}
+
+ detect-libc@2.1.2:
+ optional: true
+
+ doctrine@2.1.0:
+ dependencies:
+ esutils: 2.0.3
+
+ dom-helpers@5.2.1:
+ dependencies:
+ '@babel/runtime': 7.28.6
+ csstype: 3.2.3
+
+ dot-prop@5.3.0:
+ dependencies:
+ is-obj: 2.0.0
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ electron-to-chromium@1.5.302: {}
+
+ emoji-regex@8.0.0: {}
+
+ emoji-regex@9.2.2: {}
+
+ env-paths@2.2.1: {}
+
+ error-ex@1.3.4:
+ dependencies:
+ is-arrayish: 0.2.1
+
+ es-abstract@1.24.1:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ arraybuffer.prototype.slice: 1.0.4
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ data-view-buffer: 1.0.2
+ data-view-byte-length: 1.0.2
+ data-view-byte-offset: 1.0.1
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-set-tostringtag: 2.1.0
+ es-to-primitive: 1.3.0
+ function.prototype.name: 1.1.8
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ get-symbol-description: 1.1.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ internal-slot: 1.1.0
+ is-array-buffer: 3.0.5
+ is-callable: 1.2.7
+ is-data-view: 1.0.2
+ is-negative-zero: 2.0.3
+ is-regex: 1.2.1
+ is-set: 2.0.3
+ is-shared-array-buffer: 1.0.4
+ is-string: 1.1.1
+ is-typed-array: 1.1.15
+ is-weakref: 1.1.1
+ math-intrinsics: 1.1.0
+ object-inspect: 1.13.4
+ object-keys: 1.1.1
+ object.assign: 4.1.7
+ own-keys: 1.0.1
+ regexp.prototype.flags: 1.5.4
+ safe-array-concat: 1.1.3
+ safe-push-apply: 1.0.0
+ safe-regex-test: 1.1.0
+ set-proto: 1.0.0
+ stop-iteration-iterator: 1.1.0
+ string.prototype.trim: 1.2.10
+ string.prototype.trimend: 1.0.9
+ string.prototype.trimstart: 1.0.8
+ typed-array-buffer: 1.0.3
+ typed-array-byte-length: 1.0.3
+ typed-array-byte-offset: 1.0.4
+ typed-array-length: 1.0.7
+ unbox-primitive: 1.1.0
+ which-typed-array: 1.1.20
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-iterator-helpers@1.2.2:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-set-tostringtag: 2.1.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ internal-slot: 1.1.0
+ iterator.prototype: 1.1.5
+ safe-array-concat: 1.1.3
+
+ es-module-lexer@1.7.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ es-shim-unscopables@1.1.0:
+ dependencies:
+ hasown: 2.0.2
+
+ es-to-primitive@1.3.0:
+ dependencies:
+ is-callable: 1.2.7
+ is-date-object: 1.1.0
+ is-symbol: 1.1.1
+
+ esbuild@0.27.3:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.27.3
+ '@esbuild/android-arm': 0.27.3
+ '@esbuild/android-arm64': 0.27.3
+ '@esbuild/android-x64': 0.27.3
+ '@esbuild/darwin-arm64': 0.27.3
+ '@esbuild/darwin-x64': 0.27.3
+ '@esbuild/freebsd-arm64': 0.27.3
+ '@esbuild/freebsd-x64': 0.27.3
+ '@esbuild/linux-arm': 0.27.3
+ '@esbuild/linux-arm64': 0.27.3
+ '@esbuild/linux-ia32': 0.27.3
+ '@esbuild/linux-loong64': 0.27.3
+ '@esbuild/linux-mips64el': 0.27.3
+ '@esbuild/linux-ppc64': 0.27.3
+ '@esbuild/linux-riscv64': 0.27.3
+ '@esbuild/linux-s390x': 0.27.3
+ '@esbuild/linux-x64': 0.27.3
+ '@esbuild/netbsd-arm64': 0.27.3
+ '@esbuild/netbsd-x64': 0.27.3
+ '@esbuild/openbsd-arm64': 0.27.3
+ '@esbuild/openbsd-x64': 0.27.3
+ '@esbuild/openharmony-arm64': 0.27.3
+ '@esbuild/sunos-x64': 0.27.3
+ '@esbuild/win32-arm64': 0.27.3
+ '@esbuild/win32-ia32': 0.27.3
+ '@esbuild/win32-x64': 0.27.3
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-config-next@16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3):
+ dependencies:
+ '@next/eslint-plugin-next': 16.1.6
+ eslint: 9.39.3(jiti@2.6.1)
+ eslint-import-resolver-node: 0.3.9
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1))
+ eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.3(jiti@2.6.1))
+ eslint-plugin-react: 7.37.5(eslint@9.39.3(jiti@2.6.1))
+ eslint-plugin-react-hooks: 7.0.1(eslint@9.39.3(jiti@2.6.1))
+ globals: 16.4.0
+ typescript-eslint: 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - '@typescript-eslint/parser'
+ - eslint-import-resolver-webpack
+ - eslint-plugin-import-x
+ - supports-color
+
+ eslint-import-resolver-node@0.3.9:
+ dependencies:
+ debug: 3.2.7
+ is-core-module: 2.16.1
+ resolve: 1.22.11
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1)):
+ dependencies:
+ '@nolyfill/is-core-module': 1.0.39
+ debug: 4.4.3
+ eslint: 9.39.3(jiti@2.6.1)
+ get-tsconfig: 4.13.6
+ is-bun-module: 2.0.0
+ stable-hash: 0.0.5
+ tinyglobby: 0.2.15
+ unrs-resolver: 1.11.1
+ optionalDependencies:
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1))
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)):
+ dependencies:
+ debug: 3.2.7
+ optionalDependencies:
+ '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.3(jiti@2.6.1)
+ eslint-import-resolver-node: 0.3.9
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.3(jiti@2.6.1))
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1)):
+ dependencies:
+ '@rtsao/scc': 1.1.0
+ array-includes: 3.1.9
+ array.prototype.findlastindex: 1.2.6
+ array.prototype.flat: 1.3.3
+ array.prototype.flatmap: 1.3.3
+ debug: 3.2.7
+ doctrine: 2.1.0
+ eslint: 9.39.3(jiti@2.6.1)
+ eslint-import-resolver-node: 0.3.9
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.3(jiti@2.6.1))
+ hasown: 2.0.2
+ is-core-module: 2.16.1
+ is-glob: 4.0.3
+ minimatch: 3.1.5
+ object.fromentries: 2.0.8
+ object.groupby: 1.0.3
+ object.values: 1.2.1
+ semver: 6.3.1
+ string.prototype.trimend: 1.0.9
+ tsconfig-paths: 3.15.0
+ optionalDependencies:
+ '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ transitivePeerDependencies:
+ - eslint-import-resolver-typescript
+ - eslint-import-resolver-webpack
+ - supports-color
+
+ eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.3(jiti@2.6.1)):
+ dependencies:
+ aria-query: 5.3.2
+ array-includes: 3.1.9
+ array.prototype.flatmap: 1.3.3
+ ast-types-flow: 0.0.8
+ axe-core: 4.11.1
+ axobject-query: 4.1.0
+ damerau-levenshtein: 1.0.8
+ emoji-regex: 9.2.2
+ eslint: 9.39.3(jiti@2.6.1)
+ hasown: 2.0.2
+ jsx-ast-utils: 3.3.5
+ language-tags: 1.0.9
+ minimatch: 3.1.5
+ object.fromentries: 2.0.8
+ safe-regex-test: 1.1.0
+ string.prototype.includes: 2.0.1
+
+ eslint-plugin-react-hooks@7.0.1(eslint@9.39.3(jiti@2.6.1)):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.0
+ eslint: 9.39.3(jiti@2.6.1)
+ hermes-parser: 0.25.1
+ zod: 4.3.6
+ zod-validation-error: 4.0.2(zod@4.3.6)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-react@7.37.5(eslint@9.39.3(jiti@2.6.1)):
+ dependencies:
+ array-includes: 3.1.9
+ array.prototype.findlast: 1.2.5
+ array.prototype.flatmap: 1.3.3
+ array.prototype.tosorted: 1.1.4
+ doctrine: 2.1.0
+ es-iterator-helpers: 1.2.2
+ eslint: 9.39.3(jiti@2.6.1)
+ estraverse: 5.3.0
+ hasown: 2.0.2
+ jsx-ast-utils: 3.3.5
+ minimatch: 3.1.5
+ object.entries: 1.1.9
+ object.fromentries: 2.0.8
+ object.values: 1.2.1
+ prop-types: 15.8.1
+ resolve: 2.0.0-next.6
+ semver: 6.3.1
+ string.prototype.matchall: 4.0.12
+ string.prototype.repeat: 1.0.0
+
+ eslint-scope@8.4.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.1: {}
+
+ eslint-visitor-keys@5.0.1: {}
+
+ eslint@9.39.3(jiti@2.6.1):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1))
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.21.1
+ '@eslint/config-helpers': 0.4.2
+ '@eslint/core': 0.17.0
+ '@eslint/eslintrc': 3.3.4
+ '@eslint/js': 9.39.3
+ '@eslint/plugin-kit': 0.4.1
+ '@humanfs/node': 0.16.7
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.8
+ ajv: 6.14.0
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.5
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.6.1
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.4.0:
+ dependencies:
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ eslint-visitor-keys: 4.2.1
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ esutils@2.0.3: {}
+
+ expect-type@1.3.0: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@3.3.1:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fast-uri@3.1.0: {}
+
+ fastq@1.20.1:
+ dependencies:
+ reusify: 1.1.0
+
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ find-root@1.1.0: {}
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.3.3
+ keyv: 4.5.4
+
+ flatted@3.3.3: {}
+
+ for-each@0.3.5:
+ dependencies:
+ is-callable: 1.2.7
+
+ form-data@4.0.5:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
+ fresh@0.5.2: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ function.prototype.name@1.1.8:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ functions-have-names: 1.2.3
+ hasown: 2.0.2
+ is-callable: 1.2.7
+
+ functions-have-names@1.2.3: {}
+
+ generator-function@2.0.1: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-caller-file@2.0.5: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ get-symbol-description@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+
+ get-tsconfig@4.13.6:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
+ git-raw-commits@4.0.0:
+ dependencies:
+ dargs: 8.1.0
+ meow: 12.1.1
+ split2: 4.2.0
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ global-directory@4.0.1:
+ dependencies:
+ ini: 4.1.1
+
+ globals@14.0.0: {}
+
+ globals@16.4.0: {}
+
+ globalthis@1.0.4:
+ dependencies:
+ define-properties: 1.2.1
+ gopd: 1.2.0
+
+ gopd@1.2.0: {}
+
+ has-bigints@1.1.0: {}
+
+ has-flag@4.0.0: {}
+
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.1
+
+ has-proto@1.2.0:
+ dependencies:
+ dunder-proto: 1.0.1
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ hermes-estree@0.25.1: {}
+
+ hermes-parser@0.25.1:
+ dependencies:
+ hermes-estree: 0.25.1
+
+ hoist-non-react-statics@3.3.2:
+ dependencies:
+ react-is: 16.13.1
+
+ husky@9.1.7: {}
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ import-meta-resolve@4.2.0: {}
+
+ imurmurhash@0.1.4: {}
+
+ ini@4.1.1: {}
+
+ internal-slot@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ hasown: 2.0.2
+ side-channel: 1.1.0
+
+ is-array-buffer@3.0.5:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ is-arrayish@0.2.1: {}
+
+ is-async-function@2.1.1:
+ dependencies:
+ async-function: 1.0.0
+ call-bound: 1.0.4
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-bigint@1.1.0:
+ dependencies:
+ has-bigints: 1.1.0
+
+ is-boolean-object@1.2.2:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-bun-module@2.0.0:
+ dependencies:
+ semver: 7.7.4
+
+ is-callable@1.2.7: {}
+
+ is-core-module@2.16.1:
+ dependencies:
+ hasown: 2.0.2
+
+ is-data-view@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ is-typed-array: 1.1.15
+
+ is-date-object@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-extglob@2.1.1: {}
+
+ is-finalizationregistry@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ is-generator-function@1.1.2:
+ dependencies:
+ call-bound: 1.0.4
+ generator-function: 2.0.1
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-map@2.0.3: {}
+
+ is-negative-zero@2.0.3: {}
+
+ is-number-object@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-number@7.0.0: {}
+
+ is-obj@2.0.0: {}
+
+ is-plain-obj@4.1.0: {}
+
+ is-regex@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ is-set@2.0.3: {}
+
+ is-shared-array-buffer@1.0.4:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-string@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-symbol@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-symbols: 1.1.0
+ safe-regex-test: 1.1.0
+
+ is-typed-array@1.1.15:
+ dependencies:
+ which-typed-array: 1.1.20
+
+ is-weakmap@2.0.2: {}
+
+ is-weakref@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-weakset@2.0.4:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ isarray@2.0.5: {}
+
+ isexe@2.0.0: {}
+
+ iterator.prototype@1.1.5:
+ dependencies:
+ define-data-property: 1.1.4
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ has-symbols: 1.1.0
+ set-function-name: 2.0.2
+
+ jiti@2.6.1: {}
+
+ jose@4.15.9: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.1:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-parse-even-better-errors@2.3.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@1.0.2:
+ dependencies:
+ minimist: 1.2.8
+
+ json5@2.2.3: {}
+
+ jsx-ast-utils@3.3.5:
+ dependencies:
+ array-includes: 3.1.9
+ array.prototype.flat: 1.3.3
+ object.assign: 4.1.7
+ object.values: 1.2.1
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ language-subtag-registry@0.3.23: {}
+
+ language-tags@1.0.9:
+ dependencies:
+ language-subtag-registry: 0.3.23
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lines-and-columns@1.2.4: {}
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.camelcase@4.3.0: {}
+
+ lodash.kebabcase@4.1.1: {}
+
+ lodash.merge@4.6.2: {}
+
+ lodash.mergewith@4.6.2: {}
+
+ lodash.snakecase@4.1.1: {}
+
+ lodash.startcase@4.4.0: {}
+
+ lodash.upperfirst@4.3.1: {}
+
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
+ lowdb@7.0.1:
+ dependencies:
+ steno: 4.0.2
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ lru-cache@6.0.0:
+ dependencies:
+ yallist: 4.0.0
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ math-intrinsics@1.1.0: {}
+
+ media-typer@0.3.0: {}
+
+ memoize-one@6.0.0: {}
+
+ meow@12.1.1: {}
+
+ meow@13.2.0: {}
+
+ merge-descriptors@1.0.3: {}
+
+ merge2@1.4.1: {}
+
+ methods@1.1.2: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ mime@1.6.0: {}
+
+ minimatch@10.2.4:
+ dependencies:
+ brace-expansion: 5.0.4
+
+ minimatch@3.1.5:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ minimist@1.2.8: {}
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.11: {}
+
+ napi-postinstall@0.3.4: {}
+
+ natural-compare@1.4.0: {}
+
+ negotiator@0.6.3: {}
+
+ next-auth@4.24.13(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@8.0.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ '@babel/runtime': 7.28.6
+ '@panva/hkdf': 1.2.1
+ cookie: 0.7.2
+ jose: 4.15.9
+ next: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ oauth: 0.9.15
+ openid-client: 5.7.1
+ preact: 10.28.4
+ preact-render-to-string: 5.2.6(preact@10.28.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ uuid: 8.3.2
+ optionalDependencies:
+ nodemailer: 8.0.1
+
+ next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ '@next/env': 16.1.6
+ '@swc/helpers': 0.5.15
+ baseline-browser-mapping: 2.10.0
+ caniuse-lite: 1.0.30001775
+ postcss: 8.4.31
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4)
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 16.1.6
+ '@next/swc-darwin-x64': 16.1.6
+ '@next/swc-linux-arm64-gnu': 16.1.6
+ '@next/swc-linux-arm64-musl': 16.1.6
+ '@next/swc-linux-x64-gnu': 16.1.6
+ '@next/swc-linux-x64-musl': 16.1.6
+ '@next/swc-win32-arm64-msvc': 16.1.6
+ '@next/swc-win32-x64-msvc': 16.1.6
+ sharp: 0.34.5
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
+
+ node-exports-info@1.6.0:
+ dependencies:
+ array.prototype.flatmap: 1.3.3
+ es-errors: 1.3.0
+ object.entries: 1.1.9
+ semver: 6.3.1
+
+ node-mocks-http@1.17.2(@types/node@25.3.3):
+ dependencies:
+ accepts: 1.3.8
+ content-disposition: 0.5.4
+ depd: 1.1.2
+ fresh: 0.5.2
+ merge-descriptors: 1.0.3
+ methods: 1.1.2
+ mime: 1.6.0
+ parseurl: 1.3.3
+ range-parser: 1.2.1
+ type-is: 1.6.18
+ optionalDependencies:
+ '@types/node': 25.3.3
+
+ node-releases@2.0.27: {}
+
+ nodemailer@8.0.1: {}
+
+ nprogress@0.2.0: {}
+
+ oauth@0.9.15: {}
+
+ object-assign@4.1.1: {}
+
+ object-hash@2.2.0: {}
+
+ object-inspect@1.13.4: {}
+
+ object-keys@1.1.1: {}
+
+ object.assign@4.1.7:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+ has-symbols: 1.1.0
+ object-keys: 1.1.1
+
+ object.entries@1.1.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ object.fromentries@2.0.8:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-object-atoms: 1.1.1
+
+ object.groupby@1.0.3:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+
+ object.values@1.2.1:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ obug@2.1.1: {}
+
+ oidc-token-hash@5.2.0: {}
+
+ openid-client@5.7.1:
+ dependencies:
+ jose: 4.15.9
+ lru-cache: 6.0.0
+ object-hash: 2.2.0
+ oidc-token-hash: 5.2.0
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ own-keys@1.0.1:
+ dependencies:
+ get-intrinsic: 1.3.0
+ object-keys: 1.1.1
+ safe-push-apply: 1.0.0
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ error-ex: 1.3.4
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
+ parseurl@1.3.3: {}
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ path-parse@1.0.7: {}
+
+ path-type@4.0.0: {}
+
+ pathe@2.0.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ picomatch@4.0.3: {}
+
+ possible-typed-array-names@1.1.0: {}
+
+ postcss@8.4.31:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ preact-render-to-string@5.2.6(preact@10.28.4):
+ dependencies:
+ preact: 10.28.4
+ pretty-format: 3.8.0
+
+ preact@10.28.4: {}
+
+ prelude-ls@1.2.1: {}
+
+ prettier@3.8.1: {}
+
+ pretty-format@3.8.0: {}
+
+ prop-types@15.8.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ react-is: 16.13.1
+
+ punycode@2.3.1: {}
+
+ queue-microtask@1.2.3: {}
+
+ range-parser@1.2.1: {}
+
+ react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.4):
+ dependencies:
+ chart.js: 4.5.1
+ react: 19.2.4
+
+ react-dom@19.2.4(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ scheduler: 0.27.0
+
+ react-hook-form@7.71.2(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
+ react-is@16.13.1: {}
+
+ react-select@5.10.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ '@babel/runtime': 7.28.6
+ '@emotion/cache': 11.14.0
+ '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
+ '@floating-ui/dom': 1.7.5
+ '@types/react-transition-group': 4.4.12(@types/react@19.2.14)
+ memoize-one: 6.0.0
+ prop-types: 15.8.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4)
+ transitivePeerDependencies:
+ - '@types/react'
+ - supports-color
+
+ react-toastify@11.0.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ clsx: 2.1.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
+ react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ '@babel/runtime': 7.28.6
+ dom-helpers: 5.2.1
+ loose-envify: 1.4.0
+ prop-types: 15.8.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
+ react@19.2.4: {}
+
+ reflect.getprototypeof@1.0.10:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ which-builtin-type: 1.2.1
+
+ regexp.prototype.flags@1.5.4:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-errors: 1.3.0
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ set-function-name: 2.0.2
+
+ require-directory@2.1.1: {}
+
+ require-from-string@2.0.2: {}
+
+ resolve-from@4.0.0: {}
+
+ resolve-from@5.0.0: {}
+
+ resolve-pkg-maps@1.0.0: {}
+
+ resolve@1.22.11:
+ dependencies:
+ is-core-module: 2.16.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ resolve@2.0.0-next.6:
+ dependencies:
+ es-errors: 1.3.0
+ is-core-module: 2.16.1
+ node-exports-info: 1.6.0
+ object-keys: 1.1.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ reusify@1.1.0: {}
+
+ rollup@4.59.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.59.0
+ '@rollup/rollup-android-arm64': 4.59.0
+ '@rollup/rollup-darwin-arm64': 4.59.0
+ '@rollup/rollup-darwin-x64': 4.59.0
+ '@rollup/rollup-freebsd-arm64': 4.59.0
+ '@rollup/rollup-freebsd-x64': 4.59.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.59.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.59.0
+ '@rollup/rollup-linux-arm64-gnu': 4.59.0
+ '@rollup/rollup-linux-arm64-musl': 4.59.0
+ '@rollup/rollup-linux-loong64-gnu': 4.59.0
+ '@rollup/rollup-linux-loong64-musl': 4.59.0
+ '@rollup/rollup-linux-ppc64-gnu': 4.59.0
+ '@rollup/rollup-linux-ppc64-musl': 4.59.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.59.0
+ '@rollup/rollup-linux-riscv64-musl': 4.59.0
+ '@rollup/rollup-linux-s390x-gnu': 4.59.0
+ '@rollup/rollup-linux-x64-gnu': 4.59.0
+ '@rollup/rollup-linux-x64-musl': 4.59.0
+ '@rollup/rollup-openbsd-x64': 4.59.0
+ '@rollup/rollup-openharmony-arm64': 4.59.0
+ '@rollup/rollup-win32-arm64-msvc': 4.59.0
+ '@rollup/rollup-win32-ia32-msvc': 4.59.0
+ '@rollup/rollup-win32-x64-gnu': 4.59.0
+ '@rollup/rollup-win32-x64-msvc': 4.59.0
+ fsevents: 2.3.3
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ safe-array-concat@1.1.3:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ has-symbols: 1.1.0
+ isarray: 2.0.5
+
+ safe-buffer@5.2.1: {}
+
+ safe-push-apply@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ isarray: 2.0.5
+
+ safe-regex-test@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-regex: 1.2.1
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.7.4: {}
+
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+
+ set-function-name@2.0.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.2
+
+ set-proto@1.0.0:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+
+ sharp@0.34.5:
+ dependencies:
+ '@img/colour': 1.0.0
+ detect-libc: 2.1.2
+ semver: 7.7.4
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.34.5
+ '@img/sharp-darwin-x64': 0.34.5
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
+ '@img/sharp-libvips-darwin-x64': 1.2.4
+ '@img/sharp-libvips-linux-arm': 1.2.4
+ '@img/sharp-libvips-linux-arm64': 1.2.4
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
+ '@img/sharp-libvips-linux-s390x': 1.2.4
+ '@img/sharp-libvips-linux-x64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+ '@img/sharp-linux-arm': 0.34.5
+ '@img/sharp-linux-arm64': 0.34.5
+ '@img/sharp-linux-ppc64': 0.34.5
+ '@img/sharp-linux-riscv64': 0.34.5
+ '@img/sharp-linux-s390x': 0.34.5
+ '@img/sharp-linux-x64': 0.34.5
+ '@img/sharp-linuxmusl-arm64': 0.34.5
+ '@img/sharp-linuxmusl-x64': 0.34.5
+ '@img/sharp-wasm32': 0.34.5
+ '@img/sharp-win32-arm64': 0.34.5
+ '@img/sharp-win32-ia32': 0.34.5
+ '@img/sharp-win32-x64': 0.34.5
+ optional: true
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
+ siginfo@2.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ source-map@0.5.7: {}
+
+ split2@4.2.0: {}
+
+ stable-hash@0.0.5: {}
+
+ stackback@0.0.2: {}
+
+ std-env@3.10.0: {}
+
+ steno@4.0.2: {}
+
+ stop-iteration-iterator@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ internal-slot: 1.1.0
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string.prototype.includes@2.0.1:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+
+ string.prototype.matchall@4.0.12:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ internal-slot: 1.1.0
+ regexp.prototype.flags: 1.5.4
+ set-function-name: 2.0.2
+ side-channel: 1.1.0
+
+ string.prototype.repeat@1.0.0:
+ dependencies:
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+
+ string.prototype.trim@1.2.10:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-data-property: 1.1.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-object-atoms: 1.1.1
+ has-property-descriptors: 1.0.2
+
+ string.prototype.trimend@1.0.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ string.prototype.trimstart@1.0.8:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-bom@3.0.0: {}
+
+ strip-json-comments@3.1.1: {}
+
+ styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4):
+ dependencies:
+ client-only: 0.0.1
+ react: 19.2.4
+ optionalDependencies:
+ '@babel/core': 7.29.0
+
+ stylis@4.2.0: {}
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-preserve-symlinks-flag@1.0.0: {}
+
+ swr@2.4.1(react@19.2.4):
+ dependencies:
+ dequal: 2.0.3
+ react: 19.2.4
+ use-sync-external-store: 1.6.0(react@19.2.4)
+
+ tinybench@2.9.0: {}
+
+ tinyexec@1.0.2: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ tinyrainbow@3.0.3: {}
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ ts-api-utils@2.4.0(typescript@5.9.3):
+ dependencies:
+ typescript: 5.9.3
+
+ tsconfig-paths@3.15.0:
+ dependencies:
+ '@types/json5': 0.0.29
+ json5: 1.0.2
+ minimist: 1.2.8
+ strip-bom: 3.0.0
+
+ tslib@2.8.1: {}
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ type-is@1.6.18:
+ dependencies:
+ media-typer: 0.3.0
+ mime-types: 2.1.35
+
+ typed-array-buffer@1.0.3:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-length@1.0.3:
+ dependencies:
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-offset@1.0.4:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+ reflect.getprototypeof: 1.0.10
+
+ typed-array-length@1.0.7:
+ dependencies:
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ is-typed-array: 1.1.15
+ possible-typed-array-names: 1.1.0
+ reflect.getprototypeof: 1.0.10
+
+ typescript-eslint@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.3(jiti@2.6.1)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.9.3: {}
+
+ unbox-primitive@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-bigints: 1.1.0
+ has-symbols: 1.1.0
+ which-boxed-primitive: 1.1.1
+
+ undici-types@7.18.2: {}
+
+ unrs-resolver@1.11.1:
+ dependencies:
+ napi-postinstall: 0.3.4
+ optionalDependencies:
+ '@unrs/resolver-binding-android-arm-eabi': 1.11.1
+ '@unrs/resolver-binding-android-arm64': 1.11.1
+ '@unrs/resolver-binding-darwin-arm64': 1.11.1
+ '@unrs/resolver-binding-darwin-x64': 1.11.1
+ '@unrs/resolver-binding-freebsd-x64': 1.11.1
+ '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1
+ '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1
+ '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-arm64-musl': 1.11.1
+ '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1
+ '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-x64-gnu': 1.11.1
+ '@unrs/resolver-binding-linux-x64-musl': 1.11.1
+ '@unrs/resolver-binding-wasm32-wasi': 1.11.1
+ '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1
+ '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
+ '@unrs/resolver-binding-win32-x64-msvc': 1.11.1
+
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
+ dependencies:
+ browserslist: 4.28.1
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ use-media@1.5.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
+ use-sync-external-store@1.6.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
+ uuid@13.0.0: {}
+
+ uuid@8.3.2: {}
+
+ vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1):
+ dependencies:
+ esbuild: 0.27.3
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rollup: 4.59.0
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ '@types/node': 25.3.3
+ fsevents: 2.3.3
+ jiti: 2.6.1
+
+ vitest@4.0.18(@types/node@25.3.3)(jiti@2.6.1):
+ dependencies:
+ '@vitest/expect': 4.0.18
+ '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1))
+ '@vitest/pretty-format': 4.0.18
+ '@vitest/runner': 4.0.18
+ '@vitest/snapshot': 4.0.18
+ '@vitest/spy': 4.0.18
+ '@vitest/utils': 4.0.18
+ es-module-lexer: 1.7.0
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ obug: 2.1.1
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 1.0.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.0.3
+ vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 25.3.3
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - yaml
+
+ which-boxed-primitive@1.1.1:
+ dependencies:
+ is-bigint: 1.1.0
+ is-boolean-object: 1.2.2
+ is-number-object: 1.1.1
+ is-string: 1.1.1
+ is-symbol: 1.1.1
+
+ which-builtin-type@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ function.prototype.name: 1.1.8
+ has-tostringtag: 1.0.2
+ is-async-function: 2.1.1
+ is-date-object: 1.1.0
+ is-finalizationregistry: 1.1.1
+ is-generator-function: 1.1.2
+ is-regex: 1.2.1
+ is-weakref: 1.1.1
+ isarray: 2.0.5
+ which-boxed-primitive: 1.1.1
+ which-collection: 1.0.2
+ which-typed-array: 1.1.20
+
+ which-collection@1.0.2:
+ dependencies:
+ is-map: 2.0.3
+ is-set: 2.0.3
+ is-weakmap: 2.0.2
+ is-weakset: 2.0.4
+
+ which-typed-array@1.1.20:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ for-each: 0.3.5
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ word-wrap@1.2.5: {}
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ y18n@5.0.8: {}
+
+ yallist@3.1.1: {}
+
+ yallist@4.0.0: {}
+
+ yaml@1.10.2: {}
+
+ yargs-parser@21.1.1: {}
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+
+ yocto-queue@0.1.0: {}
+
+ zod-validation-error@4.0.2(zod@4.3.6):
+ dependencies:
+ zod: 4.3.6
+
+ zod@4.3.6: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..4de91a3
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+packages:
+ - '.'
diff --git a/public/borgwarehouse-logo-blue.svg b/public/borgwarehouse-logo-blue.svg
new file mode 100644
index 0000000..0be2a24
--- /dev/null
+++ b/public/borgwarehouse-logo-blue.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/borgwarehouse-logo-violet.svg b/public/borgwarehouse-logo-violet.svg
new file mode 100644
index 0000000..70e9352
--- /dev/null
+++ b/public/borgwarehouse-logo-violet.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/borgwarehouse-logo-white.svg b/public/borgwarehouse-logo-white.svg
new file mode 100644
index 0000000..7970f0a
--- /dev/null
+++ b/public/borgwarehouse-logo-white.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png
new file mode 100644
index 0000000..b345505
Binary files /dev/null and b/public/favicon-16x16.png differ
diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png
new file mode 100644
index 0000000..b345505
Binary files /dev/null and b/public/favicon-32x32.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
index 2a9f39d..8dee286 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/services/__mocks__/services.ts b/services/__mocks__/services.ts
new file mode 100644
index 0000000..dd7dd05
--- /dev/null
+++ b/services/__mocks__/services.ts
@@ -0,0 +1,26 @@
+export const ShellService = {
+ deleteRepo: vi.fn(),
+ updateRepo: vi.fn(),
+ createRepo: vi.fn(),
+ getLastSaveList: vi.fn(),
+ getStorageUsed: vi.fn(),
+};
+
+export const ConfigService = {
+ getUsersList: vi.fn(),
+ getRepoList: vi.fn(),
+ updateUsersList: vi.fn(),
+ updateRepoList: vi.fn(),
+};
+
+export const AuthService = {
+ verifyPassword: vi.fn(),
+ hashPassword: vi.fn(),
+ tokenController: vi.fn(),
+};
+
+export const NotifService = {
+ nodemailerSMTP: vi.fn(() => ({
+ sendMail: vi.fn().mockResolvedValue({ messageId: 'fake-message-id' }),
+ })),
+};
diff --git a/services/auth.service.test.ts b/services/auth.service.test.ts
new file mode 100644
index 0000000..9d4d962
--- /dev/null
+++ b/services/auth.service.test.ts
@@ -0,0 +1,113 @@
+import { describe, it, expect, vi } from 'vitest';
+import { AuthService } from './auth.service';
+import { ConfigService } from '~/services';
+
+vi.mock('bcryptjs', () => ({
+ hash: vi.fn().mockResolvedValue('hashedPassword'),
+ compare: vi.fn().mockResolvedValue(true),
+}));
+
+vi.mock('~/services', () => ({
+ ConfigService: {
+ getUsersList: vi.fn(),
+ },
+}));
+
+describe('AuthService', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+ });
+
+ describe('hashPassword', () => {
+ it('should hash the password correctly', async () => {
+ const password = 'testPassword';
+ const hashedPassword = await AuthService.hashPassword(password);
+ expect(hashedPassword).toBe('hashedPassword');
+ });
+ });
+
+ describe('verifyPassword', () => {
+ it('should verify the password correctly', async () => {
+ const password = 'testPassword';
+ const hashedPassword = 'hashedPassword';
+ const isValid = await AuthService.verifyPassword(password, hashedPassword);
+ expect(isValid).toBe(true);
+ });
+ });
+
+ describe('tokenController', () => {
+ it('should return undefined if DISABLE_INTEGRATIONS is true', async () => {
+ process.env.DISABLE_INTEGRATIONS = 'true';
+ const headers = { authorization: 'Bearer testToken' } as any;
+ const result = await AuthService.tokenController(headers);
+ expect(result).toBeUndefined();
+ });
+
+ it('should return undefined if no matching user is found', async () => {
+ process.env.DISABLE_INTEGRATIONS = 'false';
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([]);
+ const headers = { authorization: 'Bearer testToken' } as any;
+ const result = await AuthService.tokenController(headers);
+ expect(result).toBeUndefined();
+ });
+
+ it('should return permissions if a matching token is found', async () => {
+ process.env.DISABLE_INTEGRATIONS = 'false';
+ const mockPermissions = { read: true, create: false, update: true, delete: true };
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testUser',
+ password: 'hashedPassword',
+ email: 'testUser@example.com',
+ roles: ['user'],
+ tokens: [
+ {
+ token: 'testToken',
+ name: 'testTokenName',
+ creation: 0,
+ permissions: mockPermissions,
+ },
+ ],
+ },
+ ]);
+ const headers = { authorization: 'Bearer testToken' } as any;
+ const result = await AuthService.tokenController(headers);
+ expect(result).toEqual(mockPermissions);
+ });
+
+ it('should log and return undefined if no token matches', async () => {
+ process.env.DISABLE_INTEGRATIONS = 'false';
+ vi.mocked(ConfigService.getUsersList).mockResolvedValue([
+ {
+ id: 1,
+ username: 'testUser',
+ password: 'hashedPassword',
+ email: 'testUser@example.com',
+ roles: ['user'],
+ tokens: [
+ {
+ token: 'differentToken',
+ name: 'testTokenName',
+ permissions: { read: true, create: false, update: true, delete: true },
+ creation: 0,
+ },
+ ],
+ },
+ ]);
+ const headers = { authorization: 'Bearer testToken' } as any;
+ const result = await AuthService.tokenController(headers);
+ expect(result).toBeUndefined();
+ });
+
+ it('should throw an error if an exception occurs', async () => {
+ process.env.DISABLE_INTEGRATIONS = 'false';
+ vi.mocked(ConfigService.getUsersList).mockRejectedValue(new Error('Test error'));
+ const headers = { authorization: 'Bearer testToken' } as any;
+ await expect(AuthService.tokenController(headers)).rejects.toThrow(
+ 'Error with tokenController'
+ );
+ });
+ });
+});
diff --git a/services/auth.service.ts b/services/auth.service.ts
new file mode 100644
index 0000000..8da58ba
--- /dev/null
+++ b/services/auth.service.ts
@@ -0,0 +1,48 @@
+import { compare, hash } from 'bcryptjs';
+import { IncomingHttpHeaders } from 'http2';
+import { ConfigService } from '~/services';
+import { Optional, TokenPermissionsType } from '~/types';
+
+export const AuthService = {
+ hashPassword: async (password: string): Promise => {
+ return await hash(password, 12);
+ },
+
+ verifyPassword: async (password: string, hashedPassword: string): Promise => {
+ return await compare(password, hashedPassword);
+ },
+
+ tokenController: async (
+ headers: IncomingHttpHeaders
+ ): Promise> => {
+ const API_KEY = headers.authorization?.split(' ')[1];
+ const FROM_IP = headers['x-forwarded-for'] || 'unknown';
+
+ const timestamp = new Date().toISOString();
+
+ try {
+ if (process.env.DISABLE_INTEGRATIONS === 'true') {
+ console.log(`API auth failed from : ${FROM_IP} [${timestamp}]`);
+ return undefined;
+ }
+
+ const usersList = await ConfigService.getUsersList();
+ const user = usersList.find((u) => u.tokens?.some((t) => t.token === API_KEY));
+ if (user) {
+ const token = user.tokens?.find((token) => token.token === API_KEY);
+
+ if (token && token.permissions && typeof token.permissions === 'object') {
+ console.log(
+ `API auth success with the token '${token.name}' of user '${user.username}' from : ${FROM_IP} [${timestamp}]`
+ );
+ return token.permissions;
+ }
+ }
+
+ console.log(`API auth failed from : ${FROM_IP} [${timestamp}]`);
+ return undefined;
+ } catch (error) {
+ throw new Error('Error with tokenController');
+ }
+ },
+};
diff --git a/services/config.service.ts b/services/config.service.ts
new file mode 100644
index 0000000..0dde9f1
--- /dev/null
+++ b/services/config.service.ts
@@ -0,0 +1,106 @@
+import { Low } from 'lowdb';
+import { promises as fs } from 'fs';
+import { JSONFile } from 'lowdb/node';
+import path from 'path';
+import { Mutex } from 'async-mutex';
+import { BorgWarehouseUser, Repository } from '~/types';
+
+const jsonDirectory = path.join(process.cwd(), '/config');
+const usersDbPath = path.join(jsonDirectory, 'users.json');
+const repoDbPath = path.join(jsonDirectory, 'repo.json');
+
+// Lowdb config
+const usersAdapter = new JSONFile(usersDbPath);
+const usersDb = new Low(usersAdapter, []);
+
+const repoAdapter = new JSONFile(repoDbPath);
+const repoDb = new Low(repoAdapter, []);
+
+// Mutexes for concurrent access
+const usersMutex = new Mutex();
+const repoMutex = new Mutex();
+
+export const ConfigService = {
+ getUsersList: async (): Promise => {
+ try {
+ await usersMutex.runExclusive(async () => {
+ await usersDb.read();
+ });
+ return usersDb.data;
+ } catch (error) {
+ console.log('Error reading users.json:', error);
+ return [];
+ }
+ },
+
+ updateUsersList: async (usersList: BorgWarehouseUser[]): Promise => {
+ try {
+ await usersMutex.runExclusive(async () => {
+ usersDb.data = usersList;
+ await usersDb.write();
+ });
+ } catch (error) {
+ console.log('Error writing users.json:', error);
+ }
+ },
+
+ getRepoList: async (): Promise => {
+ try {
+ await repoMutex.runExclusive(async () => {
+ await repoDb.read();
+ });
+ return repoDb.data;
+ } catch (error) {
+ console.log('Error reading repo.json:', error);
+ return [];
+ }
+ },
+
+ updateRepoList: async (repoList: Repository[], history = false): Promise => {
+ try {
+ await repoMutex.runExclusive(async () => {
+ if (history) {
+ await repoHistory(repoList);
+ }
+ repoDb.data = repoList;
+ await repoDb.write();
+ });
+ } catch (error) {
+ console.log('Error writing repo.json:', error);
+ }
+ },
+};
+
+// Repository history management
+async function repoHistory(repoList: Repository[]) {
+ try {
+ const repoHistoryDir = path.join(process.cwd(), '/config/versions');
+ const maxBackupCount = parseInt(process.env.MAX_REPO_BACKUP_COUNT ?? '8', 10);
+ const timestamp = new Date().toISOString();
+ const backupDate = timestamp.split('T')[0];
+
+ //Create the directory if it does not exist
+ await fs.mkdir(repoHistoryDir, { recursive: true });
+
+ const existingBackups = await fs.readdir(repoHistoryDir);
+
+ if (existingBackups.length >= maxBackupCount) {
+ existingBackups.sort();
+ const backupsToDelete = existingBackups.slice(0, existingBackups.length - maxBackupCount + 1);
+ for (const backupToDelete of backupsToDelete) {
+ const backupFilePathToDelete = path.join(repoHistoryDir, backupToDelete);
+ await fs.unlink(backupFilePathToDelete);
+ }
+ }
+
+ const backupFileName = `${backupDate}.log`;
+ const backupFilePath = path.join(repoHistoryDir, backupFileName);
+ const jsonData = JSON.stringify(repoList, null, 2);
+
+ const logData = `\n>>>> History of file repo.json at "${timestamp}" <<<<\n${jsonData}\n`;
+
+ await fs.appendFile(backupFilePath, logData);
+ } catch (error) {
+ console.log('An error occurred while saving the repo history :', error);
+ }
+}
diff --git a/services/index.ts b/services/index.ts
new file mode 100644
index 0000000..c5f70c3
--- /dev/null
+++ b/services/index.ts
@@ -0,0 +1,4 @@
+export * from './auth.service';
+export * from './config.service';
+export * from './shell.service';
+export * from './notif.service';
diff --git a/services/notif.service.ts b/services/notif.service.ts
new file mode 100644
index 0000000..daa07b2
--- /dev/null
+++ b/services/notif.service.ts
@@ -0,0 +1,29 @@
+import nodemailer, { Transporter } from 'nodemailer';
+import SMTPTransport from 'nodemailer/lib/smtp-transport';
+
+export const NotifService = {
+ nodemailerSMTP(): Transporter {
+ const config: SMTPTransport.Options = {
+ port: parseInt(process.env.MAIL_SMTP_PORT || '587', 10),
+ host: process.env.MAIL_SMTP_HOST,
+ tls: {
+ // false value allow self-signed or invalid TLS certificate
+ rejectUnauthorized: process.env.MAIL_REJECT_SELFSIGNED_TLS === 'false' ? false : true,
+ },
+ };
+
+ const smtpLogin = process.env.MAIL_SMTP_LOGIN || '';
+ const smtpPwd = process.env.MAIL_SMTP_PWD || '';
+
+ // Some SMTP servers doesn't require authentication #364
+ if (smtpLogin) {
+ config.auth = {
+ user: smtpLogin,
+ pass: smtpPwd,
+ };
+ }
+
+ const transporter: Transporter = nodemailer.createTransport(config);
+ return transporter;
+ },
+};
diff --git a/services/shell.service.ts b/services/shell.service.ts
new file mode 100644
index 0000000..49d155f
--- /dev/null
+++ b/services/shell.service.ts
@@ -0,0 +1,91 @@
+import path from 'path';
+import { promisify } from 'util';
+import { execFile as execFileCallback } from 'node:child_process';
+import { LastSaveDTO, StorageUsedDTO } from '~/types';
+import repositoryNameCheck from '~/helpers/functions/repositoryNameCheck';
+
+const execFile = promisify(execFileCallback);
+const shellsDirectory = path.join(process.cwd(), '/helpers/shells');
+
+// This is to prevent the cronjob from being executed multiple times
+let isLastSaveListRunning = false;
+let isStorageUsedRunning = false;
+
+function isValidSshKey(key: string): boolean {
+ return /^ssh-(rsa|ed25519|ed25519-sk) [A-Za-z0-9+/=]+(\s.+)?$/.test(key.trim());
+}
+
+export const ShellService = {
+ getLastSaveList: async (): Promise => {
+ if (isLastSaveListRunning) {
+ throw new Error('The check status service is already running');
+ } else {
+ isLastSaveListRunning = true;
+ }
+
+ try {
+ const { stdout } = await execFile(`${shellsDirectory}/getLastSave.sh`);
+ return JSON.parse(stdout || '[]');
+ } finally {
+ isLastSaveListRunning = false;
+ }
+ },
+
+ getStorageUsed: async (): Promise => {
+ if (isStorageUsedRunning) {
+ throw new Error('The storage used service is already running');
+ } else {
+ isStorageUsedRunning = true;
+ }
+ try {
+ const { stdout } = await execFile(`${shellsDirectory}/getStorageUsed.sh`);
+ return JSON.parse(stdout || '[]');
+ } finally {
+ isStorageUsedRunning = false;
+ }
+ },
+
+ deleteRepo: async (repositoryName: string) => {
+ const { stdout, stderr } = await execFile(`${shellsDirectory}/deleteRepo.sh`, [repositoryName]);
+ return { stdout, stderr };
+ },
+
+ updateRepo: async (
+ repositoryName: string,
+ sshPublicKey: string,
+ storageSize: number,
+ appendOnlyMode: boolean
+ ) => {
+ if (!isValidSshKey(sshPublicKey)) {
+ throw new Error('Invalid SSH key format');
+ }
+ if (!repositoryNameCheck(repositoryName)) {
+ throw new Error('Invalid repository name format');
+ }
+
+ const { stdout, stderr } = await execFile(`${shellsDirectory}/updateRepo.sh`, [
+ repositoryName,
+ sshPublicKey,
+ storageSize.toString(),
+ appendOnlyMode.toString(),
+ ]);
+ return { stdout, stderr };
+ },
+
+ createRepo: async (
+ sshPublicKey: string,
+ storageSize: number,
+ appendOnlyMode: boolean
+ ): Promise<{ stdout?: string; stderr?: string }> => {
+ if (!isValidSshKey(sshPublicKey)) {
+ throw new Error('Invalid SSH key format');
+ }
+
+ const { stdout, stderr } = await execFile(`${shellsDirectory}/createRepo.sh`, [
+ sshPublicKey,
+ storageSize.toString(),
+ appendOnlyMode.toString(),
+ ]);
+ return { stdout, stderr };
+ },
+};
diff --git a/styles/default.css b/styles/default.css
index a48897e..77f5905 100644
--- a/styles/default.css
+++ b/styles/default.css
@@ -1,213 +1,196 @@
* {
- box-sizing: border-box;
+ box-sizing: border-box;
}
@font-face {
- font-family: 'Inter';
- src: url('/font/Inter/Inter-VariableFont_slnt,wght.ttf') format('truetype');
+ font-family: 'Inter';
+ src: url('/font/Inter/Inter-VariableFont_slnt,wght.ttf') format('truetype');
}
body {
- margin: 0;
- font-family: Inter, sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- background-color: #fafafa;
+ margin: 0;
+ font-family: Inter, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: #fafafa;
}
/* Disable scrollbar for chrome */
::-webkit-scrollbar {
- display: none;
+ display: none;
+}
+
+#nprogress {
+ pointer-events: none;
+}
+
+#nprogress .bar {
+ background: #6d4aff;
+ position: fixed;
+ z-index: 9999;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 2px;
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
.defaultButton {
- border: 0;
- padding: 10px 15px;
- background-color: #6d4aff;
- color: white;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-weight: bold;
- font-size: 1em;
+ border: 0;
+ padding: 10px 16px;
+ background-color: #6d4aff;
+ color: white;
+ border-radius: 8px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: 600;
+ font-size: 1rem;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
+ transition: all 0.2s ease-in-out;
}
.defaultButton:hover {
- border: 0;
- padding: 10px 15px;
- background-color: #4f31ce;
- color: white;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-weight: bold;
- font-size: 1em;
+ background-color: #5a3de0;
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.08);
}
.defaultButton:active {
- border: 0;
- padding: 10px 15px;
- background-color: #4f31ce;
- color: white;
- border-radius: 4px;
- cursor: pointer;
- text-decoration: none;
- font-weight: bold;
- font-size: 1em;
- transform: scale(0.9);
+ background-color: #4f31ce;
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.05);
+ /* Pas de scale */
}
.defaultButton:disabled {
- opacity: 0.3;
- cursor: not-allowed;
- pointer-events: none;
+ opacity: 0.4;
+ cursor: not-allowed;
+ pointer-events: none;
+ transform: none;
+ box-shadow: none;
}
/* signIn */
.signInContainer {
- display: 'flex';
- flex-direction: 'column';
- justify-content: 'center';
- align-items: 'center';
- height: calc(100vh - 100px);
- animation: signInAnim 1s ease 0s 1 normal forwards;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: calc(100vh - 100px);
+ animation: fadeIn 0.5s ease-out forwards;
+ opacity: 0;
}
-@keyframes signInAnim {
- 0% {
- opacity: 0;
- transform: translateY(-250px);
- }
-
- 100% {
- opacity: 1;
- transform: translateY(0);
- }
+@keyframes fadeIn {
+ to {
+ opacity: 1;
+ }
}
.signInInput {
- background: #262e49;
- background-color: #262e49;
- border: 1px solid #6d4aff21;
- font-size: 16px;
- height: auto;
- margin: 0;
- margin-bottom: 0px;
- outline: 0;
- padding: 15px;
- width: 100%;
- border-radius: 5px;
- color: #d6d6d6;
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
+ background: #262e49;
+ background-color: #262e49;
+ border: 1px solid #6d4aff21;
+ font-size: 16px;
+ height: auto;
+ margin: 0;
+ margin-bottom: 0px;
+ outline: 0;
+ padding: 15px;
+ width: 100%;
+ border-radius: 5px;
+ color: #d6d6d6;
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
}
.signInInput:focus {
- outline: 1px solid #6d4aff;
- box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
+ outline: 1px solid #6d4aff;
+ box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
}
.signInInput.invalid {
- background: #f3c7c7;
- border: 1px solid #e45454;
- outline: 1px solid #ff4a4a;
+ background: #f3c7c7;
+ border: 1px solid #e45454;
+ outline: 1px solid #ff4a4a;
}
.signInInput.invalid:focus {
- background: #f3c7c7;
- border: 1px solid #e45454;
- outline: 1px solid #ff4a4a;
- box-shadow: 0 0 10px 3px rgba(255, 74, 74, 0.605);
+ background: #f3c7c7;
+ border: 1px solid #e45454;
+ outline: 1px solid #ff4a4a;
+ box-shadow: 0 0 10px 3px rgba(255, 74, 74, 0.605);
}
.signInButton {
- border: 0;
- padding: 10px 15px;
- background-color: #6d4aff;
- color: #dfdeee;
- margin: 5px;
- border-radius: 100px;
- cursor: pointer;
- text-decoration: none;
- font-size: 1em;
+ width: 100%;
+ height: 40px;
+ padding: 10px;
+ background-color: #704dff;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ font-weight: bold;
+ cursor: pointer;
+ font-size: 0.9em;
+ transition: background 0.2s ease-in-out;
}
.signInButton:hover {
- border: 0;
- padding: 10px 15px;
- background-color: #4f31ce;
- color: #dfdeee;
- margin: 5px;
- border-radius: 100px;
- cursor: pointer;
- text-decoration: none;
- font-size: 1em;
+ background-color: #4f31ce;
}
.signInButton:active {
- border: 0;
- padding: 10px 15px;
- background-color: #4f31ce;
- color: #dfdeee;
- margin: 5px;
- border-radius: 100px;
- cursor: pointer;
- text-decoration: none;
- font-size: 1em;
- transform: scale(0.9);
+ transform: scale(0.95);
}
.signInButton:disabled {
- opacity: 0.3;
- cursor: not-allowed;
- pointer-events: none;
+ opacity: 0.3;
+ cursor: not-allowed;
+ pointer-events: none;
}
.heart {
- color: #6d4aff;
+ color: #6d4aff;
}
.heart::before {
- content: '\f004';
+ content: '\f004';
}
/* Radio group and radio button */
.radio-group {
- display: flex;
+ display: flex;
}
.radio-group input[type='radio']:checked + span::before {
- background-color: #6d4aff;
- box-shadow: inset white 0 0 0 2px;
+ background-color: #6d4aff;
+ box-shadow: inset white 0 0 0 2px;
}
.radio-group span::before {
- border: 2px solid #6d4aff;
- content: '';
- display: flex;
- height: 16px;
- width: 16px;
- margin: 0 5px;
- border-radius: 50%;
- box-sizing: border-box;
- transition: all ease 0.2s;
- box-shadow: inset white 0 0 0 10px;
+ border: 2px solid #6d4aff;
+ content: '';
+ display: flex;
+ height: 16px;
+ width: 16px;
+ margin: 0 5px;
+ border-radius: 50%;
+ box-sizing: border-box;
+ transition: all ease 0.2s;
+ box-shadow: inset white 0 0 0 10px;
}
.radio-group label {
- margin: 0 8px 0 0;
- cursor: pointer;
+ margin: 0 8px 0 0;
+ cursor: pointer;
}
.radio-group input[type='radio'] {
- opacity: 0;
- width: 0;
+ opacity: 0;
+ width: 0;
}
.radio-group span {
- display: flex;
- align-items: center;
+ display: flex;
+ align-items: center;
}
diff --git a/tests/README b/tests/README
new file mode 100644
index 0000000..e9a892c
--- /dev/null
+++ b/tests/README
@@ -0,0 +1,3 @@
+## BATS tests against bash scripts
+
+From `tests/bats`, launch `docker compose up --build`
diff --git a/tests/bats/Dockerfile b/tests/bats/Dockerfile
new file mode 100644
index 0000000..0717179
--- /dev/null
+++ b/tests/bats/Dockerfile
@@ -0,0 +1,19 @@
+FROM bash:latest
+
+RUN apk add --no-cache \
+ bats \
+ openssl \
+ borgbackup \
+ jq \
+ coreutils
+
+COPY helpers/shells/ /test/scripts/
+COPY tests/bats/createRepo.bats /test/tests/createRepo.bats
+COPY tests/bats/deleteRepo.bats /test/tests/deleteRepo.bats
+COPY tests/bats/updateRepo.bats /test/tests/updateRepo.bats
+COPY tests/bats/getLastSave.bats /test/tests/getLastSave.bats
+COPY tests/bats/getStorageUsed.bats /test/tests/getStorageUsed.bats
+
+RUN chmod +x /test/scripts/*.sh
+
+CMD ["bats", "/test/tests/"]
diff --git a/tests/bats/createRepo.bats b/tests/bats/createRepo.bats
new file mode 100644
index 0000000..b82467e
--- /dev/null
+++ b/tests/bats/createRepo.bats
@@ -0,0 +1,104 @@
+#!/usr/bin/env bats
+
+setup() {
+ # Set up the environment for each test
+ export home="/tmp/borgwarehouse"
+ mkdir -p /tmp/borgwarehouse
+ mkdir -p /tmp/borgwarehouse/repos
+ mkdir -p /tmp/borgwarehouse/.ssh
+ touch /tmp/borgwarehouse/.ssh/authorized_keys
+
+ # SSH keys samples for testing
+ export SSH_KEY_ED25519="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICtujwncNGgdNxWOCQedMCnrhRZT4B7eUyyFJNryvQj9 publickey"
+ export SSH_KEY_ED25519_SK="sk-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICtujwncNGgdNxWOCQedMCnrhRZT4B7eUyyFJNryvQj9 publickey"
+ export SSH_KEY_RSA="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDf8SFSuWqPtPYKjoXL8aowdKYfeKFKbE6w4CvqXPSRtgwKGWJva/UVF8Q/jGClwsVpTJfZnA76fnih76cE4ZiucPtDM2dyDILHNSZo/8rwUVkNB4P3aaCxV6lVMurmIgF4ibWQFBdyWKCJM7nQjO71TlMw/HfqpeYXXdjL1MBlMzqOZYLDrPoiJEiAfKheVeCONMlo8HMfEPxiu7bwfF7vQqYstcbZ55RN1t7RYaxlCTZaj0GOxIGKLmmHTDGzQQIaOGSr3+8Gk1I/MFle2/dYKbBEi97NrJowRO4a4pVbVso0YKyESL3U40uZly1bzoNx4DvMBbFwYSE1IJbs/AQIfB6KH4yLtQTmfb4qPRLCS1CBWBZKeKJ304p6rxKuv+CjagsFwdG5cS7cCosfdEU43QuWngnYQGUwMKskxX/7rPm+WZItN7XiNoMRmzaC+T0cIRXH7Cl7VFE3cbTzgmqJPVeUpccjTP/BdDahHKFqyVhAFvyI7JM4ct1/tU8o015TM1EXzMBeJOxalj6RsuDIFjjEaMN5pZmlHGBFBmcRgY7TqYAwr02maKb9BtcPOGIPgpI3AMzqNX+LjFssI0AuqBGTYN8v6OBr2NmTHfZlnClucjoAw71QPeQySABqrX0p9xieX15Ly1z9oMH9lapW6X9e0JnQBMnz1N2eaq1qAQ== publickey"
+}
+
+teardown() {
+ # Clean up the environment after each test
+ rm -rf /tmp/borgwarehouse
+}
+
+@test "Test createRepo.sh with missing arguments" {
+ run bash /test/scripts/createRepo.sh
+ [ "$status" -eq 1 ]
+ [ "$output" == "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]" ]
+}
+
+@test "Test createRepo.sh with missing Quota and append-only mode arguments" {
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_ED25519"
+ [ "$status" -eq 1 ]
+ [ "$output" == "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]" ]
+}
+
+@test "Test createRepo.sh with missing Append-only mode argument" {
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_ED25519" 10
+ echo $output
+ cat ${home}/.ssh/authorized_keys
+ [ "$status" -eq 1 ]
+ [ "$output" == "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]" ]
+}
+
+@test "Test createRepo.sh with invalid SSH key format" {
+ run bash /test/scripts/createRepo.sh "invalid-key" 10 true
+ [ "$status" -eq 2 ]
+ [ "$output" == "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)" ]
+}
+
+@test "Test createRepo.sh with invalid Quota format" {
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_ED25519" "AA" true
+ [ "$status" -eq 1 ]
+ [ "$output" == "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]" ]
+}
+
+@test "Test createRepo.sh with invalid Append-only mode format" {
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_ED25519" 10 blabla
+ [ "$status" -eq 1 ]
+ [ "$output" == "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]" ]
+}
+
+
+@test "Test createRepo.sh if authorized_keys is missing" {
+ rm /tmp/borgwarehouse/.ssh/authorized_keys
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_ED25519" 10 true
+ [ "$status" -eq 5 ]
+ [ "$output" == "${home}/.ssh/authorized_keys must be present" ]
+}
+
+@test "Test createRepo.sh if SSH key is already present in authorized_keys" {
+ # Add a key
+ echo "$SSH_KEY_ED25519" > /tmp/borgwarehouse/.ssh/authorized_keys
+ # Try to re-add the same key
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_ED25519" 10 true
+ [ "$status" -eq 3 ]
+ [ "$output" == "SSH pub key already present in authorized_keys" ]
+}
+
+@test "Test createRepo.sh repository name generation" {
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_ED25519" 10 false
+ [[ "$output" =~ ^[0-9a-f]{8}$ ]] # Must return a 8 characters hexa string
+}
+
+@test "Test createRepo.sh key ED25519 insertion in authorized_keys" {
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_ED25519" 10 false
+ expected_line="command=\"cd ${home}/repos;borg serve --restrict-to-repository ${home}/repos/${output} --storage-quota 10G\",restrict $SSH_KEY_ED25519"
+ grep -qF "$expected_line" /tmp/borgwarehouse/.ssh/authorized_keys
+}
+
+@test "Test createRepo.sh key ED25519-SK insertion in authorized_keys" {
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_ED25519_SK" 10 false
+ expected_line="command=\"cd ${home}/repos;borg serve --restrict-to-repository ${home}/repos/${output} --storage-quota 10G\",restrict $SSH_KEY_ED25519_SK"
+ grep -qF "$expected_line" /tmp/borgwarehouse/.ssh/authorized_keys
+}
+
+@test "Test createRepo.sh key RSA insertion in authorized_keys" {
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_RSA" 10 false
+ expected_line="command=\"cd ${home}/repos;borg serve --restrict-to-repository ${home}/repos/${output} --storage-quota 10G\",restrict $SSH_KEY_RSA"
+ grep -qF "$expected_line" /tmp/borgwarehouse/.ssh/authorized_keys
+}
+
+@test "Test createRepo.sh key ED25519 insertion in authorized_keys with append only mode" {
+ run bash /test/scripts/createRepo.sh "$SSH_KEY_ED25519" 10 true
+ expected_line="command=\"cd ${home}/repos;borg serve --append-only --restrict-to-repository ${home}/repos/${output} --storage-quota 10G\",restrict $SSH_KEY_ED25519"
+ grep -qF "$expected_line" /tmp/borgwarehouse/.ssh/authorized_keys
+}
diff --git a/tests/bats/deleteRepo.bats b/tests/bats/deleteRepo.bats
new file mode 100644
index 0000000..bbc6a2b
--- /dev/null
+++ b/tests/bats/deleteRepo.bats
@@ -0,0 +1,90 @@
+#!/usr/bin/env bats
+
+setup() {
+ # Setup the environment for each test
+ export home="/tmp/borgwarehouse"
+ mkdir -p "${home}/repos"
+ mkdir -p "${home}/.ssh"
+ touch "${home}/.ssh/authorized_keys"
+
+ # SSH keys samples for testing
+ export SSH_KEY_ED25519="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICtujwncNGgdNxWOCQedMCnrhRZT4B7eUyyFJNryvQj9 publickey"
+}
+
+teardown() {
+ # Cleanup after each test
+ rm -rf /tmp/borgwarehouse
+}
+
+@test "Test deleteRepo.sh with missing arguments" {
+ run bash /test/scripts/deleteRepo.sh
+ [ "$status" -eq 1 ]
+ [ "$output" == "You must provide a repositoryName in argument." ]
+}
+
+@test "Test deleteRepo.sh with repositoryName shorter than 8 characters" {
+ run bash /test/scripts/deleteRepo.sh "1234567"
+ [ "$status" -eq 2 ]
+ [ "$output" == "Invalid repository name. Must be an 8-character hex string." ]
+}
+
+@test "Test deleteRepo.sh with repositoryName longer than 8 characters" {
+ run bash /test/scripts/deleteRepo.sh "123456789"
+ [ "$status" -eq 2 ]
+ [ "$output" == "Invalid repository name. Must be an 8-character hex string." ]
+}
+
+@test "Test deleteRepo.sh with unexpected character in repositoryName" {
+ run bash /test/scripts/deleteRepo.sh "ffff/123"
+ [ "$status" -eq 2 ]
+ [ "$output" == "Invalid repository name. Must be an 8-character hex string." ]
+}
+
+@test "Test deleteRepo.sh for non-existing repository and associated key" {
+ # Add an SSH key to authorized_keys without creating the repository
+ echo "command=\"cd ${home}/repos;borg serve --restrict-to-path ${home}/repos/abcdef12 --storage-quota 10G\",restrict $SSH_KEY_ED25519" >> "${home}/.ssh/authorized_keys"
+
+ run bash /test/scripts/deleteRepo.sh "abcdef12"
+
+ [ "$status" -eq 0 ]
+ [ "$output" == "The folder ${home}/repos/abcdef12 did not exist (repository never initialized or used). The line associated in the authorized_keys file has been deleted." ]
+
+ # Check that the line was removed from authorized_keys
+ ! grep -q "abcdef12" "${home}/.ssh/authorized_keys"
+}
+
+@test "Test deleteRepo.sh for existing repository but no associated key in authorized_keys" {
+ # Create a repository folder without adding the corresponding entry in authorized_keys
+ mkdir -p "${home}/repos/abcdef13"
+
+ run bash /test/scripts/deleteRepo.sh "abcdef13"
+
+ [ "$status" -eq 0 ]
+ [ "$output" == "The folder ${home}/repos/abcdef13 and all its data have been deleted. The line associated in the authorized_keys file has been deleted." ]
+
+ # Check that the repository folder is deleted
+ [ ! -d "${home}/repos/abcdef13" ]
+
+ # Check that no line was present in authorized_keys to begin with (and nothing was affected)
+ ! grep -q "abcdef13" "${home}/.ssh/authorized_keys"
+}
+
+@test "Test deleteRepo.sh for existing repository and associated key" {
+ # Create a repository folder and add a corresponding entry in authorized_keys
+ mkdir -p "${home}/repos/abcdef12"
+ echo "command=\"cd ${home}/repos;borg serve --restrict-to-path ${home}/repos/abcdef12 --storage-quota 10G\",restrict $SSH_KEY_ED25519" >> "${home}/.ssh/authorized_keys"
+
+ run bash /test/scripts/deleteRepo.sh "abcdef12"
+
+ [ "$status" -eq 0 ]
+ [ "$output" == "The folder ${home}/repos/abcdef12 and all its data have been deleted. The line associated in the authorized_keys file has been deleted." ]
+
+ # Check that the repository folder is deleted
+ [ ! -d "${home}/repos/abcdef12" ]
+
+ # Check that the line was removed from authorized_keys
+ ! grep -q "abcdef12" "${home}/.ssh/authorized_keys"
+}
+
+
+
diff --git a/tests/bats/docker-compose.yml b/tests/bats/docker-compose.yml
new file mode 100644
index 0000000..b0c472b
--- /dev/null
+++ b/tests/bats/docker-compose.yml
@@ -0,0 +1,8 @@
+services:
+ bats-test:
+ build:
+ context: ../..
+ dockerfile: tests/bats/Dockerfile
+ volumes:
+ - ../../helpers/shells:/test/scripts:ro
+ container_name: bats-test-container
diff --git a/tests/bats/getLastSave.bats b/tests/bats/getLastSave.bats
new file mode 100644
index 0000000..c6d2322
--- /dev/null
+++ b/tests/bats/getLastSave.bats
@@ -0,0 +1,85 @@
+#!/usr/bin/env bats
+
+setup() {
+ export home="/tmp/borgwarehouse"
+ mkdir -p "${home}/repos/repo1"
+ mkdir -p "${home}/repos/repo2"
+
+ # Create integrity files
+ touch "${home}/repos/repo1/integrity_file_1"
+ touch "${home}/repos/repo2/integrity_file_2"
+
+ # Set the last modified time of the integrity files
+ touch -d '2024-09-04 12:00:00' "${home}/repos/repo1/integrity_file_1"
+ touch -d '2024-10-04 13:00:00' "${home}/repos/repo2/integrity_file_2"
+
+ echo "home=${home}" > "${home}/.env"
+}
+
+teardown() {
+ rm -rf /tmp/borgwarehouse
+}
+
+@test "Test getLastSave.sh - Get last save timestamps for existing repositories" {
+ run bash /test/scripts/getLastSave.sh
+
+ # expected output in JSON format
+ expected_output='[
+ {
+ "repositoryName": "repo1",
+ "lastSave": 1725451200
+ },
+ {
+ "repositoryName": "repo2",
+ "lastSave": 1728046800
+ }
+ ]'
+
+ # Normalize the JSON output for comparison
+ run_output=$(echo "$output" | jq .)
+ normalized_expected_output=$(echo "$expected_output" | jq .)
+
+ [ "$run_output" = "$normalized_expected_output" ]
+}
+
+@test "Test getLastSave.sh - Get last save timestamps when no repositories exist" {
+ # Delete all repositories
+ rm -rf "${home}/repos"
+ mkdir -p "${home}/repos"
+
+ run bash /test/scripts/getLastSave.sh
+
+ expected_output='[]'
+
+ run_output=$(echo "$output" | jq .)
+ normalized_expected_output=$(echo "$expected_output" | jq .)
+
+ [ "$status" -eq 0 ]
+ [ "$run_output" = "$normalized_expected_output" ]
+}
+
+@test "Test getLastSave.sh - Get last save timestamps with missing integrity file" {
+ rm "${home}/repos/repo1/integrity_file_1"
+
+ run bash /test/scripts/getLastSave.sh
+
+ expected_output='[
+ {
+ "repositoryName": "repo2",
+ "lastSave": 1728046800
+ }
+ ]'
+
+ run_output=$(echo "$output" | jq .)
+ normalized_expected_output=$(echo "$expected_output" | jq .)
+
+ [ "$status" -eq 0 ]
+ [ "$run_output" = "$normalized_expected_output" ]
+}
+
+@test "Test getLastSave.sh - Check .env loading" {
+ rm "${home}/.env" # Remove .env to check default behavior
+
+ run bash /test/scripts/getLastSave.sh
+ [ "$status" -eq 0 ]
+}
diff --git a/tests/bats/getStorageUsed.bats b/tests/bats/getStorageUsed.bats
new file mode 100644
index 0000000..9913371
--- /dev/null
+++ b/tests/bats/getStorageUsed.bats
@@ -0,0 +1,107 @@
+#!/usr/bin/env bats
+
+setup() {
+ export home="/tmp/borgwarehouse"
+ mkdir -p "${home}/repos/repo1"
+ mkdir -p "${home}/repos/repo2"
+ mkdir -p "${home}/repos/repo3"
+
+ # Create files with different sizes
+ dd if=/dev/zero of="${home}/repos/repo1/file1" bs=1K count=32
+ dd if=/dev/zero of="${home}/repos/repo2/file1" bs=1K count=1156
+ dd if=/dev/zero of="${home}/repos/repo3/file1" bs=1K count=112
+
+ echo "home=${home}" > "${home}/.env"
+}
+
+teardown() {
+ rm -rf /tmp/borgwarehouse
+}
+
+@test "Test getStorageUsed.sh returns the size of all repositories in JSON format" {
+ run bash /test/scripts/getStorageUsed.sh
+
+ # Expected output in JSON format with my fake files
+ expected_output='[
+ {
+ "size": 36,
+ "name": "repo1"
+ },
+ {
+ "size": 1160,
+ "name": "repo2"
+ },
+ {
+ "size": 116,
+ "name": "repo3"
+ }
+ ]'
+
+ normalized_output=$(echo "$output" | jq .)
+ normalized_expected_output=$(echo "$expected_output" | jq .)
+
+ [ "$status" -eq 0 ]
+ [ "$normalized_output" == "$normalized_expected_output" ]
+}
+
+
+@test "Test getStorageUsed.sh when no repositories exist" {
+ # Delete all repositories
+ rm -rf "${home}/repos"
+ mkdir -p "${home}/repos"
+
+ run bash /test/scripts/getStorageUsed.sh
+
+ normalized_expected_output='[]'
+ normalized_output=$(echo "$output" | jq .)
+
+ [ "$status" -eq 0 ]
+ [ "$normalized_output" == "$normalized_expected_output" ]
+}
+
+@test "Test getStorageUsed.sh with only one repository" {
+ # Keep only one repository
+ rm -rf "${home}/repos/repo2" "${home}/repos/repo3"
+
+ run bash /test/scripts/getStorageUsed.sh
+
+ expected_output='[{"size": 36, "name": "repo1"}]'
+
+ normalized_output=$(echo "$output" | jq .)
+ normalized_expected_output=$(echo "$expected_output" | jq .)
+
+ echo "$normalized_output"
+ echo "$normalized_expected_output"
+
+ [ "$status" -eq 0 ]
+ [ "$normalized_output" == "$normalized_expected_output" ]
+}
+
+@test "Test getStorageUsed.sh ignores lost+found directory" {
+ mkdir -p "${home}/repos/lost+found"
+ dd if=/dev/zero of="${home}/repos/lost+found/file1" bs=1K count=500
+
+ run bash /test/scripts/getStorageUsed.sh
+
+ # Expected output should NOT include lost+found
+ expected_output='[
+ {
+ "size": 36,
+ "name": "repo1"
+ },
+ {
+ "size": 1160,
+ "name": "repo2"
+ },
+ {
+ "size": 116,
+ "name": "repo3"
+ }
+ ]'
+
+ normalized_output=$(echo "$output" | jq .)
+ normalized_expected_output=$(echo "$expected_output" | jq .)
+
+ [ "$status" -eq 0 ]
+ [ "$normalized_output" == "$normalized_expected_output" ]
+}
\ No newline at end of file
diff --git a/tests/bats/updateRepo.bats b/tests/bats/updateRepo.bats
new file mode 100644
index 0000000..908c70d
--- /dev/null
+++ b/tests/bats/updateRepo.bats
@@ -0,0 +1,84 @@
+#!/usr/bin/env bats
+
+setup() {
+ # Setup the environment for each test
+ export home="/tmp/borgwarehouse"
+ mkdir -p "${home}/repos"
+ mkdir -p "${home}/.ssh"
+ touch "${home}/.ssh/authorized_keys"
+
+ # SSH keys samples for testing
+ export SSH_KEY_ED25519="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICtujwncNGgdNxWOCQedMCnrhRZT4B7eUyyFJNryvQj9 publickey"
+ export SSH_KEY_RSA="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDf8SFSuWqPtPYKjoXL8aowdKYfeKFKbE6w4CvqXPSRtgwKGWJva/UVF8Q/jGClwsVpTJfZnA76fnih76cE4ZiucPtDM2dyDILHNSZo/8rwUVkNB4P3aaCxV6lVMurmIgF4ibWQFBdyWKCJM7nQjO71TlMw/HfqpeYXXdjL1MBlMzqOZYLDrPoiJEiAfKheVeCONMlo8HMfEPxiu7bwfF7vQqYstcbZ55RN1t7RYaxlCTZaj0GOxIGKLmmHTDGzQQIaOGSr3+8Gk1I/MFle2/dYKbBEi97NrJowRO4a4pVbVso0YKyESL3U40uZly1bzoNx4DvMBbFwYSE1IJbs/AQIfB6KH4yLtQTmfb4qPRLCS1CBWBZKeKJ304p6rxKuv+CjagsFwdG5cS7cCosfdEU43QuWngnYQGUwMKskxX/7rPm+WZItN7XiNoMRmzaC+T0cIRXH7Cl7VFE3cbTzgmqJPVeUpccjTP/BdDahHKFqyVhAFvyI7JM4ct1/tU8o015TM1EXzMBeJOxalj6RsuDIFjjEaMN5pZmlHGBFBmcRgY7TqYAwr02maKb9BtcPOGIPgpI3AMzqNX+LjFssI0AuqBGTYN8v6OBr2NmTHfZlnClucjoAw71QPeQySABqrX0p9xieX15Ly1z9oMH9lapW6X9e0JnQBMnz1N2eaq1qAQ== publickey"
+}
+
+teardown() {
+ # Cleanup after each test
+ rm -rf /tmp/borgwarehouse
+}
+
+@test "Test updateRepo.sh with missing arguments" {
+ run bash /test/scripts/updateRepo.sh
+ [ "$status" -eq 1 ]
+ [ "$output" == "This shell takes 4 args: [repositoryName] [new SSH pub key] [quota] [Append only mode [true|false]]" ]
+}
+
+@test "Test updateRepo.sh with invalid SSH key format" {
+ run bash /test/scripts/updateRepo.sh "abcdef12" "invalid-key" 10 true
+ [ "$status" -eq 2 ]
+ [ "$output" == "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)" ]
+}
+
+@test "Test updateRepo.sh with repositoryName shorter than 8 characters" {
+ run bash /test/scripts/updateRepo.sh "1234567" "$SSH_KEY_ED25519" 10 true
+ [ "$status" -eq 3 ]
+ [ "$output" == "Invalid repository name. Must be an 8-character hex string." ]
+}
+
+@test "Test updateRepo.sh with repositoryName not matching hex format" {
+ run bash /test/scripts/updateRepo.sh "invalid" "$SSH_KEY_ED25519" 10 true
+ [ "$status" -eq 3 ]
+ [ "$output" == "Invalid repository name. Must be an 8-character hex string." ]
+}
+
+@test "Test updateRepo.sh when no matching repository name in authorized_keys" {
+ run bash /test/scripts/updateRepo.sh "abcdef12" "$SSH_KEY_ED25519" 10 true
+ [ "$status" -eq 4 ]
+ [ "$output" == "No line containing abcdef12 found in authorized_keys" ]
+}
+
+@test "Test updateRepo.sh when the new SSH key is already present for a different repository" {
+ # Add the new key to a different repository
+ echo "command=\"cd ${home}/repos;borg serve --restrict-to-path ${home}/repos/abcdef13 --storage-quota 10G\",restrict $SSH_KEY_ED25519" >> "${home}/.ssh/authorized_keys"
+
+ # Add an entry for the repository being updated
+ echo "command=\"cd ${home}/repos;borg serve --restrict-to-path ${home}/repos/abcdef12 --storage-quota 10G\",restrict $SSH_KEY_RSA" >> "${home}/.ssh/authorized_keys"
+
+ run bash /test/scripts/updateRepo.sh "abcdef12" "$SSH_KEY_ED25519" 10 true
+ [ "$status" -eq 5 ]
+ [ "$output" == "This SSH pub key is already present in authorized_keys on a different line." ]
+}
+
+@test "Test updateRepo.sh with successful update and append-only enabled" {
+ # Add an entry for the repository being updated
+ echo "command=\"cd ${home}/repos;borg serve --restrict-to-path ${home}/repos/abcdef12 --storage-quota 10G\",restrict $SSH_KEY_RSA" >> "${home}/.ssh/authorized_keys"
+
+ # Update the repository with a new SSH key and append-only mode
+ run bash /test/scripts/updateRepo.sh "abcdef12" "$SSH_KEY_ED25519" 20 true
+ [ "$status" -eq 0 ]
+
+ # Check that the line was updated correctly in authorized_keys
+ grep -q "command=\"cd ${home}/repos;borg serve --append-only --restrict-to-path ${home}/repos/abcdef12 --storage-quota 20G\",restrict $SSH_KEY_ED25519" "${home}/.ssh/authorized_keys"
+}
+
+@test "Test updateRepo.sh with disabling append-only mode" {
+ # Add an entry for the repository being updated with append-only enabled
+ echo "command=\"cd ${home}/repos;borg serve --append-only --restrict-to-path ${home}/repos/abcdef12 --storage-quota 10G\",restrict $SSH_KEY_RSA" >> "${home}/.ssh/authorized_keys"
+
+ # Update the repository with a new SSH key and disable append-only mode
+ run bash /test/scripts/updateRepo.sh "abcdef12" "$SSH_KEY_ED25519" 20 false
+ [ "$status" -eq 0 ]
+
+ # Check that the append-only mode was removed
+ grep -q "command=\"cd ${home}/repos;borg serve --restrict-to-path ${home}/repos/abcdef12 --storage-quota 20G\",restrict $SSH_KEY_ED25519" "${home}/.ssh/authorized_keys"
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..6c45e20
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,37 @@
+{
+ "compilerOptions": {
+ "paths": {
+ "~/*": [
+ "./*"
+ ]
+ },
+ "target": "ES2017",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "incremental": true,
+ "module": "esnext",
+ "esModuleInterop": true,
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "types": [
+ "vitest/globals"
+ ] // Auto import vitest types
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/types/api/error.types.ts b/types/api/error.types.ts
new file mode 100644
index 0000000..37502d8
--- /dev/null
+++ b/types/api/error.types.ts
@@ -0,0 +1,14 @@
+export type ErrorResponse = {
+ status?: number;
+ message: string;
+};
+
+export type SuccessResponse = {
+ message?: string;
+};
+
+export type BorgWarehouseApiResponse = {
+ status: number;
+ message: string;
+ data?: T;
+};
diff --git a/types/api/index.ts b/types/api/index.ts
new file mode 100644
index 0000000..e4baa50
--- /dev/null
+++ b/types/api/index.ts
@@ -0,0 +1,6 @@
+export * from './error.types';
+export * from './integration.types';
+export * from './next-auth.types';
+export * from './notification.types';
+export * from './setting.types';
+export * from './shell.types';
diff --git a/types/api/integration.types.ts b/types/api/integration.types.ts
new file mode 100644
index 0000000..e09cb6e
--- /dev/null
+++ b/types/api/integration.types.ts
@@ -0,0 +1,21 @@
+export type IntegrationTokenType = {
+ token: string;
+ name: string;
+ creation: number;
+ expiration?: number;
+ permissions: TokenPermissionsType;
+};
+
+export type TokenPermissionsType = {
+ create: boolean;
+ read: boolean;
+ update: boolean;
+ delete: boolean;
+};
+
+export enum TokenPermissionEnum {
+ CREATE = 'create',
+ READ = 'read',
+ UPDATE = 'update',
+ DELETE = 'delete',
+}
diff --git a/types/api/next-auth.types.ts b/types/api/next-auth.types.ts
new file mode 100644
index 0000000..9351011
--- /dev/null
+++ b/types/api/next-auth.types.ts
@@ -0,0 +1,22 @@
+import { DefaultSession, DefaultUser } from 'next-auth';
+
+declare module 'next-auth' {
+ // Add custom properties to the User object in the session
+ interface User extends DefaultUser {
+ roles?: string[];
+ id?: string;
+ }
+
+ interface Session {
+ user: {
+ roles?: string[];
+ id?: string;
+ } & DefaultSession['user'];
+ }
+}
+
+export enum SessionStatus {
+ AUTHENTICATED = 'authenticated',
+ UNAUTHENTICATED = 'unauthenticated',
+ LOADING = 'loading',
+}
diff --git a/types/api/notification.types.ts b/types/api/notification.types.ts
new file mode 100644
index 0000000..4a3af63
--- /dev/null
+++ b/types/api/notification.types.ts
@@ -0,0 +1,22 @@
+import { AppriseModeEnum } from '../domain/config.types';
+
+export type AppriseAlertResponse = {
+ appriseAlert?: boolean;
+};
+
+export type EmailAlertDTO = {
+ emailAlert?: boolean;
+};
+
+export type AppriseAlertDTO = {
+ appriseAlert: boolean;
+};
+
+export type AppriseModeDTO = {
+ appriseMode?: AppriseModeEnum;
+ appriseStatelessURL?: string;
+};
+
+export type AppriseServicesDTO = {
+ appriseServices?: string[];
+};
diff --git a/types/api/setting.types.ts b/types/api/setting.types.ts
new file mode 100644
index 0000000..fae2dc6
--- /dev/null
+++ b/types/api/setting.types.ts
@@ -0,0 +1,12 @@
+export type EmailSettingDTO = {
+ email?: string;
+};
+
+export type UsernameSettingDTO = {
+ username?: string;
+};
+
+export type PasswordSettingDTO = {
+ oldPassword: string;
+ newPassword: string;
+};
diff --git a/types/api/shell.types.ts b/types/api/shell.types.ts
new file mode 100644
index 0000000..ff1a85d
--- /dev/null
+++ b/types/api/shell.types.ts
@@ -0,0 +1,9 @@
+export type LastSaveDTO = {
+ repositoryName: string;
+ lastSave: number;
+};
+
+export type StorageUsedDTO = {
+ size: number;
+ name: string;
+};
diff --git a/types/domain/config.types.ts b/types/domain/config.types.ts
new file mode 100644
index 0000000..6a7ecca
--- /dev/null
+++ b/types/domain/config.types.ts
@@ -0,0 +1,54 @@
+import { IntegrationTokenType } from '../api/integration.types';
+
+export type Repository = {
+ id: number;
+ alias: string;
+ repositoryName: string;
+ status: boolean;
+ lastSave: number;
+ alert?: number;
+ storageSize: number;
+ storageUsed: number;
+ sshPublicKey: string;
+ comment: string;
+ displayDetails?: boolean; // @deprecated
+ unixUser?: string; // @deprecated
+ lanCommand?: boolean;
+ appendOnlyMode?: boolean;
+ lastStatusAlertSend?: number;
+};
+
+export type BorgWarehouseUser = {
+ id: number;
+ username: string;
+ password: string;
+ roles: string[];
+ email: string;
+ emailAlert?: boolean;
+ appriseAlert?: boolean;
+ appriseMode?: AppriseModeEnum;
+ appriseStatelessURL?: string;
+ appriseServices?: string[];
+ tokens?: Array;
+};
+
+export enum WizardEnvEnum {
+ UNIX_USER = 'UNIX_USER',
+ FQDN = 'FQDN',
+ SSH_SERVER_PORT = 'SSH_SERVER_PORT',
+ FQDN_LAN = 'FQDN_LAN',
+ SSH_SERVER_PORT_LAN = 'SSH_SERVER_PORT_LAN',
+ SSH_SERVER_FINGERPRINT_RSA = 'SSH_SERVER_FINGERPRINT_RSA',
+ SSH_SERVER_FINGERPRINT_ECDSA = 'SSH_SERVER_FINGERPRINT_ECDSA',
+ SSH_SERVER_FINGERPRINT_ED25519 = 'SSH_SERVER_FINGERPRINT_ED25519',
+ HIDE_SSH_PORT = 'HIDE_SSH_PORT',
+ DISABLE_INTEGRATIONS = 'DISABLE_INTEGRATIONS',
+ DISABLE_DELETE_REPO = 'DISABLE_DELETE_REPO',
+}
+
+export type WizardEnvType = Record;
+
+export enum AppriseModeEnum {
+ PACKAGE = 'package',
+ STATELESS = 'stateless',
+}
diff --git a/types/domain/constants.ts b/types/domain/constants.ts
new file mode 100644
index 0000000..0150d47
--- /dev/null
+++ b/types/domain/constants.ts
@@ -0,0 +1,16 @@
+export const alertOptions = [
+ { value: 0, label: 'Disabled' },
+ { value: 3600, label: '1 hour' },
+ { value: 21600, label: '6 hours' },
+ { value: 43200, label: '12 hours' },
+ { value: 90000, label: '1 day' },
+ { value: 172800, label: '2 days' },
+ { value: 259200, label: '3 days' },
+ { value: 345600, label: '4 days' },
+ { value: 432000, label: '5 days' },
+ { value: 518400, label: '6 days' },
+ { value: 604800, label: '7 days' },
+ { value: 864000, label: '10 days' },
+ { value: 1209600, label: '14 days' },
+ { value: 2592000, label: '30 days' },
+];
diff --git a/types/domain/index.ts b/types/domain/index.ts
new file mode 100644
index 0000000..c9f8316
--- /dev/null
+++ b/types/domain/index.ts
@@ -0,0 +1,3 @@
+export * from './config.types';
+export * from './constants';
+export * from './wizard.types';
diff --git a/types/domain/wizard.types.ts b/types/domain/wizard.types.ts
new file mode 100644
index 0000000..0cc0483
--- /dev/null
+++ b/types/domain/wizard.types.ts
@@ -0,0 +1,14 @@
+import { WizardEnvType } from '~/types/domain/config.types';
+
+export type SelectedRepoWizard = {
+ label: string;
+ value: string;
+ id: string;
+ repositoryName: string;
+ lanCommand: boolean;
+};
+
+export type WizardStepProps = {
+ selectedRepo?: SelectedRepoWizard;
+ wizardEnv?: WizardEnvType;
+};
diff --git a/types/index.ts b/types/index.ts
new file mode 100644
index 0000000..104d184
--- /dev/null
+++ b/types/index.ts
@@ -0,0 +1,3 @@
+export * from './optional';
+export * from './api';
+export * from './domain';
diff --git a/types/optional.ts b/types/optional.ts
new file mode 100644
index 0000000..964b0f2
--- /dev/null
+++ b/types/optional.ts
@@ -0,0 +1 @@
+export type Optional = T | undefined;
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..a687812
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,28 @@
+// vitest.config.ts
+import { defineConfig } from 'vitest/config';
+import path from 'path';
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html'],
+ },
+ globals: true,
+ include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ exclude: [
+ '**/node_modules/**',
+ '**/dist/**',
+ '**/.{idea,git,cache,output,temp}/**',
+ '**/.next/**',
+ '**/build/**',
+ '**/repos/**',
+ ],
+ },
+ resolve: {
+ alias: {
+ '~': path.resolve(__dirname, './'),
+ },
+ },
+});