mirror of
https://github.com/gethomepage/homepage.git
synced 2026-02-08 17:00:51 +08:00
Compare commits
16 Commits
l10n_dev
...
feature/au
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea5d031d1f | ||
|
|
83b5e96682 | ||
|
|
8792337133 | ||
|
|
42b290c76c | ||
|
|
c4afced5fa | ||
|
|
6b6457cb5d | ||
|
|
ab869f042a | ||
|
|
e0b66c398f | ||
|
|
d55ef5cb9c | ||
|
|
abb8d50327 | ||
|
|
cddc9dacf8 | ||
|
|
28db90521f | ||
|
|
dffd21b600 | ||
|
|
e375a9747a | ||
|
|
f0e65a6ac8 | ||
|
|
0660b91d94 |
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -35,8 +35,8 @@ What type of change does your PR introduce to Homepage?
|
|||||||
## Checklist:
|
## Checklist:
|
||||||
|
|
||||||
- [ ] If applicable, I have added corresponding documentation changes.
|
- [ ] If applicable, I have added corresponding documentation changes.
|
||||||
- [ ] If applicable, I have added or updated tests for new features and bug fixes (see [testing](https://gethomepage.dev/widgets/authoring/getting-started/#testing)).
|
- [ ] If applicable, I have added or updated tests for new features and bug fixes.
|
||||||
- [ ] If applicable, I have reviewed the [feature / enhancement](https://gethomepage.dev/widgets/authoring/getting-started/#new-feature-guidelines) and / or [service widget guidelines](https://gethomepage.dev/widgets/authoring/getting-started/#service-widget-guidelines).
|
- [ ] If applicable, I have reviewed the [feature / enhancement](https://gethomepage.dev/more/development/#new-feature-guidelines) and / or [service widget guidelines](https://gethomepage.dev/more/development/#service-widget-guidelines).
|
||||||
- [ ] I have checked that all code style checks pass using [pre-commit hooks](https://gethomepage.dev/widgets/authoring/getting-started/#code-formatting-with-pre-commit-hooks) and [linting checks](https://gethomepage.dev/widgets/authoring/getting-started/#code-linting).
|
- [ ] I have checked that all code style checks pass using [pre-commit hooks](https://gethomepage.dev/more/development/#code-formatting-with-pre-commit-hooks) and [linting checks](https://gethomepage.dev/more/development/#code-linting).
|
||||||
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||||
- [ ] In the description above I have disclosed the use of AI tools in the coding of this PR.
|
- [ ] In the description above I have disclosed the use of AI tools in the coding of this PR.
|
||||||
|
|||||||
50
.github/workflows/docs-publish.yml
vendored
50
.github/workflows/docs-publish.yml
vendored
@@ -9,9 +9,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-commit:
|
pre-commit:
|
||||||
@@ -37,34 +35,44 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
python-version: 3.x
|
||||||
- name: Install uv
|
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||||
uses: astral-sh/setup-uv@v7
|
- uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
key: mkdocs-material-${{ env.cache_id }}
|
||||||
|
path: .cache
|
||||||
|
restore-keys: |
|
||||||
|
mkdocs-material-
|
||||||
- run: sudo apt-get install pngquant
|
- run: sudo apt-get install pngquant
|
||||||
|
- run: pip install mkdocs-material mkdocs-redirects "mkdocs-material[imaging]"
|
||||||
- name: Test Docs Build
|
- name: Test Docs Build
|
||||||
run: uv run --frozen zensical build --clean
|
run: MKINSIDERS=false mkdocs build
|
||||||
deploy:
|
deploy:
|
||||||
name: Build & Deploy Docs
|
name: Build & Deploy Docs
|
||||||
if: github.repository == 'gethomepage/homepage' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.repository == 'gethomepage/homepage' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
needs:
|
needs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/configure-pages@v5
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
- name: Configure Git Credentials
|
||||||
|
run: |
|
||||||
|
git config user.name github-actions[bot]
|
||||||
|
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
python-version: 3.x
|
||||||
- name: Install uv
|
- run: echo "cache_id=${{github.sha}}" >> $GITHUB_ENV
|
||||||
uses: astral-sh/setup-uv@v7
|
- uses: actions/cache@v5
|
||||||
- run: sudo apt-get install pngquant
|
|
||||||
- name: Build Docs
|
|
||||||
run: uv run --frozen zensical build --clean
|
|
||||||
- uses: actions/upload-pages-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
path: site
|
key: mkdocs-material-${{ env.cache_id }}
|
||||||
- uses: actions/deploy-pages@v4
|
path: .cache
|
||||||
id: deployment
|
restore-keys: |
|
||||||
|
mkdocs-material-
|
||||||
|
- run: sudo apt-get install pngquant
|
||||||
|
- run: pip install git+https://${GH_TOKEN}@github.com/benphelps/mkdocs-material-insiders.git
|
||||||
|
- run: pip install mkdocs-redirects "mkdocs-material[imaging]"
|
||||||
|
- name: Docs Deploy
|
||||||
|
run: MKINSIDERS=true mkdocs gh-deploy --force
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -13,13 +13,13 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
shard: [1, 2, 3, 4]
|
shard: [1, 2, 3, 4]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 9
|
version: 9
|
||||||
|
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -46,7 +46,7 @@ next-env.d.ts
|
|||||||
# IDEs
|
# IDEs
|
||||||
/.idea/
|
/.idea/
|
||||||
|
|
||||||
# Zensical documentation
|
# MkDocs documentation
|
||||||
site*/
|
site*/
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
3.13
|
|
||||||
67
README.md
67
README.md
@@ -70,65 +70,14 @@ For configuration options, examples and more, [please check out the homepage doc
|
|||||||
|
|
||||||
## Security Notice 🔒
|
## Security Notice 🔒
|
||||||
|
|
||||||
Please note that when using features such as widgets, Homepage can access personal information (for example from your home automation system) and Homepage currently does not (and is not planned to) include any authentication layer itself. If Homepage is reachable from any untrusted network, it **must** sit behind a reverse proxy (and/or VPN) that enforces authentication, TLS, and strictly validates Host headers. The built-in host check in Homepage is a best-effort guard and should not be treated as security when exposed publicly.
|
Please note that when using features such as widgets, Homepage can access personal information (for example from your home automation system). To keep your information private, if Homepage is reachable from any untrusted network, it:
|
||||||
|
|
||||||
## With Docker
|
1. **must** sit behind a reverse proxy (and/or VPN) that enforces authentication, TLS, and strictly validates Host headers.
|
||||||
|
2. An optional built-in OIDC login flow is available (opt-in) offering a simple “authenticated or not” guard.
|
||||||
|
|
||||||
Using docker compose:
|
## Installation
|
||||||
|
|
||||||
```yaml
|
See the [Installation](https://gethomepage.dev/installation/) section of the docs for instructions on installing Homepage via Docker, Kubernetes, Unraid, or from source.
|
||||||
services:
|
|
||||||
homepage:
|
|
||||||
image: ghcr.io/gethomepage/homepage:latest
|
|
||||||
container_name: homepage
|
|
||||||
environment:
|
|
||||||
HOMEPAGE_ALLOWED_HOSTS: gethomepage.dev # required, may need port. See gethomepage.dev/installation/#homepage_allowed_hosts
|
|
||||||
PUID: 1000 # optional, your user id
|
|
||||||
PGID: 1000 # optional, your group id
|
|
||||||
ports:
|
|
||||||
- 3000:3000
|
|
||||||
volumes:
|
|
||||||
- /path/to/config:/app/config # Make sure your local config directory exists
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro # optional, for docker integrations
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
or docker run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --name homepage \
|
|
||||||
-e HOMEPAGE_ALLOWED_HOSTS=gethomepage.dev \
|
|
||||||
-e PUID=1000 \
|
|
||||||
-e PGID=1000 \
|
|
||||||
-p 3000:3000 \
|
|
||||||
-v /path/to/config:/app/config \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
|
||||||
--restart unless-stopped \
|
|
||||||
ghcr.io/gethomepage/homepage:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## From Source
|
|
||||||
|
|
||||||
First, clone the repository:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/gethomepage/homepage.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Then install dependencies and build the production bundle:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
If this is your first time starting, copy the `src/skeleton` directory to `config/` to populate initial example config files.
|
|
||||||
|
|
||||||
Finally, run the server in production mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm start
|
|
||||||
```
|
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
@@ -156,16 +105,16 @@ This is a [Next.js](https://nextjs.org/) application, see their documentation fo
|
|||||||
|
|
||||||
The homepage documentation is available at [https://gethomepage.dev/](https://gethomepage.dev/).
|
The homepage documentation is available at [https://gethomepage.dev/](https://gethomepage.dev/).
|
||||||
|
|
||||||
Homepage uses Zensical for documentation. To run the documentation locally, first install the dependencies:
|
Homepage uses Material for MkDocs for documentation. To run the documentation locally, first install the dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run the development server:
|
Then run the development server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run zensical serve # or build, to build the static site
|
mkdocs serve # or build, to build the static site
|
||||||
```
|
```
|
||||||
|
|
||||||
# Support & Suggestions
|
# Support & Suggestions
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /path/to/config:/app/config # Make sure your local config directory exists
|
- /path/to/config:/app/config # Make sure your local config directory exists
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro # (optional) For docker integrations
|
- /var/run/docker.sock:/var/run/docker.sock:ro # (optional) For docker integrations
|
||||||
environment:
|
|
||||||
HOMEPAGE_ALLOWED_HOSTS: gethomepage.dev # required, may need port. See gethomepage.dev/installation/#homepage_allowed_hosts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running as non-root
|
### Running as non-root
|
||||||
@@ -38,7 +36,6 @@ services:
|
|||||||
- /path/to/config:/app/config # Make sure your local config directory exists
|
- /path/to/config:/app/config # Make sure your local config directory exists
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro # (optional) For docker integrations, see alternative methods
|
- /var/run/docker.sock:/var/run/docker.sock:ro # (optional) For docker integrations, see alternative methods
|
||||||
environment:
|
environment:
|
||||||
HOMEPAGE_ALLOWED_HOSTS: gethomepage.dev # required, may need port. See gethomepage.dev/installation/#homepage_allowed_hosts
|
|
||||||
PUID: $PUID
|
PUID: $PUID
|
||||||
PGID: $PGID
|
PGID: $PGID
|
||||||
```
|
```
|
||||||
@@ -46,7 +43,7 @@ services:
|
|||||||
### With Docker Run
|
### With Docker Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 3000:3000 -e HOMEPAGE_ALLOWED_HOSTS=gethomepage.dev -v /path/to/config:/app/config -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/gethomepage/homepage:latest
|
docker run -p 3000:3000 -v /path/to/config:/app/config -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/gethomepage/homepage:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Environment Secrets
|
### Using Environment Secrets
|
||||||
|
|||||||
@@ -27,14 +27,25 @@ You have a few options for deploying homepage, depending on your needs. We offer
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### `HOMEPAGE_ALLOWED_HOSTS`
|
### Security & Authentication
|
||||||
|
|
||||||
As of v1.0 there is one required environment variable to access homepage via a URL other than `localhost`, <code>HOMEPAGE_ALLOWED_HOSTS</code>. The setting helps prevent certain kinds of attacks when retrieving data from the homepage API proxy.
|
Public deployments of Homepage should be secured via a reverse proxy, VPN, or similar. As of version 2.0, Homepage supports a simple authorization gate with a password or OIDC. When enabled, Homepage will use password login by default unless OIDC variables are provided.
|
||||||
|
|
||||||
The value is a comma-separated (no spaces) list of allowed hosts (sometimes with the port) that can host your homepage install. See the [docker](docker.md), [kubernetes](k8s.md) and [source](source.md) installation pages for more information about where / how to set the variable.
|
Required environment variables for authentication:
|
||||||
|
|
||||||
`localhost:3000` and `127.0.0.1:3000` are always included, but you can add a domain or IP address to this list to allow that host such as `HOMEPAGE_ALLOWED_HOSTS=gethomepage.dev,192.168.1.2:1234`, etc.
|
- `HOMEPAGE_AUTH_ENABLED=true`
|
||||||
|
- `HOMEPAGE_AUTH_SECRET` (random string for signing/encrypting cookies)
|
||||||
|
|
||||||
If you are seeing errors about host validation, check the homepage logs and ensure that the host exactly as output in the logs is in the `HOMEPAGE_ALLOWED_HOSTS` list.
|
For password-only login:
|
||||||
|
|
||||||
This can be disabled by setting `HOMEPAGE_ALLOWED_HOSTS` to `*` but this is not recommended. Public deployments must rely on a reverse proxy (and/or VPN) that enforces authentication, TLS, and unexpected Host headers; the built-in host check is a best-effort guard for local setups and is not a substitute for edge protections.
|
- `HOMEPAGE_AUTH_PASSWORD` (password-only login; required unless OIDC settings are provided)
|
||||||
|
|
||||||
|
For OIDC login (overrides password login):
|
||||||
|
|
||||||
|
- `HOMEPAGE_OIDC_ISSUER` (OIDC issuer URL, e.g., `https://auth.example.com/realms/homepage`)
|
||||||
|
- `HOMEPAGE_OIDC_CLIENT_ID`
|
||||||
|
- `HOMEPAGE_OIDC_CLIENT_SECRET`
|
||||||
|
- `HOMEPAGE_EXTERNAL_URL` (external URL to your Homepage instance; used for callbacks)
|
||||||
|
- Optional: `HOMEPAGE_OIDC_NAME` (display name), `HOMEPAGE_OIDC_SCOPE` (defaults to `openid email profile`)
|
||||||
|
|
||||||
|
All app pages and `/api` routes will require a signed-in session. Static assets remain public. Homepage still does not implement per-user dashboards or roles; authentication is a simple gate only.
|
||||||
|
|||||||
@@ -223,9 +223,6 @@ spec:
|
|||||||
- name: homepage
|
- name: homepage
|
||||||
image: "ghcr.io/gethomepage/homepage:latest"
|
image: "ghcr.io/gethomepage/homepage:latest"
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
env:
|
|
||||||
- name: HOMEPAGE_ALLOWED_HOSTS
|
|
||||||
value: gethomepage.dev # required, may need port. See gethomepage.dev/installation/#homepage_allowed_hosts
|
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 3000
|
containerPort: 3000
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ If this is your first time starting, copy the `src/skeleton` directory to `confi
|
|||||||
Finally, run the server:
|
Finally, run the server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
HOMEPAGE_ALLOWED_HOSTS=gethomepage.dev:1234 pnpm start
|
pnpm start
|
||||||
```
|
```
|
||||||
|
|
||||||
When updating homepage versions you will need to re-build the static files i.e. repeat the process above.
|
When updating homepage versions you will need to re-build the static files i.e. repeat the process above.
|
||||||
|
|
||||||
See [HOMEPAGE_ALLOWED_HOSTS](index.md#homepage_allowed_hosts) for more information on this environment variable.
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description: Jellyfin Widget Configuration
|
|||||||
|
|
||||||
Learn more about [Jellyfin](https://github.com/jellyfin/jellyfin).
|
Learn more about [Jellyfin](https://github.com/jellyfin/jellyfin).
|
||||||
|
|
||||||
You can create an API key from inside the Jellyfin Administration Dashboard under `Advanced > API Keys`.
|
You can create an API key from inside Jellyfin at `Settings > Advanced > Api Keys`.
|
||||||
|
|
||||||
As of v0.6.11 the widget supports fields `["movies", "series", "episodes", "songs"]`. These blocks are disabled by default but can be enabled with the `enableBlocks` option, and the "Now Playing" feature (enabled by default) can be disabled with the `enableNowPlaying` option.
|
As of v0.6.11 the widget supports fields `["movies", "series", "episodes", "songs"]`. These blocks are disabled by default but can be enabled with the `enableBlocks` option, and the "Now Playing" feature (enabled by default) can be disabled with the `enableNowPlaying` option.
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ As of v0.6.11 the widget supports fields `["movies", "series", "episodes", "song
|
|||||||
```yaml
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
type: jellyfin
|
type: jellyfin
|
||||||
url: http://jellyfin.host.or.ip:port
|
url: http://jellyfin.host.or.ip
|
||||||
key: apikeyapikeyapikeyapikeyapikey
|
key: apikeyapikeyapikeyapikeyapikey
|
||||||
version: 2 # optional, default is 1
|
version: 2 # optional, default is 1
|
||||||
enableBlocks: true # optional, defaults to false
|
enableBlocks: true # optional, defaults to false
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Allowed fields: no configurable fields for this widget.
|
|||||||
```yaml
|
```yaml
|
||||||
widget:
|
widget:
|
||||||
type: tautulli
|
type: tautulli
|
||||||
url: http://tautulli.host.or.ip:port
|
url: http://tautulli.host.or.ip
|
||||||
key: apikeyapikeyapikeyapikeyapikey
|
key: apikeyapikeyapikeyapikeyapikey
|
||||||
enableUser: true # optional, defaults to false
|
enableUser: true # optional, defaults to false
|
||||||
showEpisodeNumber: true # optional, defaults to false
|
showEpisodeNumber: true # optional, defaults to false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "1.10.1",
|
"version": "1.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"minecraftstatuspinger": "^1.2.2",
|
"minecraftstatuspinger": "^1.2.2",
|
||||||
"next": "^15.5.11",
|
"next": "^15.5.11",
|
||||||
|
"next-auth": "^4.24.10",
|
||||||
"next-i18next": "^12.1.0",
|
"next-i18next": "^12.1.0",
|
||||||
"ping": "^0.4.4",
|
"ping": "^0.4.4",
|
||||||
"pretty-bytes": "^7.1.0",
|
"pretty-bytes": "^7.1.0",
|
||||||
|
|||||||
114
pnpm-lock.yaml
generated
114
pnpm-lock.yaml
generated
@@ -53,6 +53,9 @@ importers:
|
|||||||
next:
|
next:
|
||||||
specifier: ^15.5.11
|
specifier: ^15.5.11
|
||||||
version: 15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
next-auth:
|
||||||
|
specifier: ^4.24.10
|
||||||
|
version: 4.24.13(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
next-i18next:
|
next-i18next:
|
||||||
specifier: ^12.1.0
|
specifier: ^12.1.0
|
||||||
version: 12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -803,6 +806,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
||||||
engines: {node: '>=12.4.0'}
|
engines: {node: '>=12.4.0'}
|
||||||
|
|
||||||
|
'@panva/hkdf@1.2.1':
|
||||||
|
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -1683,6 +1689,10 @@ packages:
|
|||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
|
cookie@0.7.2:
|
||||||
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
core-js@3.40.0:
|
core-js@3.40.0:
|
||||||
resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==}
|
resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==}
|
||||||
|
|
||||||
@@ -2551,6 +2561,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jose@4.15.9:
|
||||||
|
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
|
||||||
|
|
||||||
jose@5.10.0:
|
jose@5.10.0:
|
||||||
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
||||||
|
|
||||||
@@ -2723,6 +2736,10 @@ packages:
|
|||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
|
lru-cache@6.0.0:
|
||||||
|
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
luxon@3.6.1:
|
luxon@3.6.1:
|
||||||
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
|
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2831,6 +2848,20 @@ packages:
|
|||||||
net@1.0.2:
|
net@1.0.2:
|
||||||
resolution: {integrity: sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==}
|
resolution: {integrity: sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==}
|
||||||
|
|
||||||
|
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-i18next@12.1.0:
|
next-i18next@12.1.0:
|
||||||
resolution: {integrity: sha512-rhos/PVULmZPdC0jpec2MDBQMXdGZ3+Mbh/tZfrDtjgnVN3ucdq7k8BlwsJNww6FnqC8AC31n6dSYuqVzYsGsw==}
|
resolution: {integrity: sha512-rhos/PVULmZPdC0jpec2MDBQMXdGZ3+Mbh/tZfrDtjgnVN3ucdq7k8BlwsJNww6FnqC8AC31n6dSYuqVzYsGsw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2878,10 +2909,17 @@ packages:
|
|||||||
oauth4webapi@3.3.0:
|
oauth4webapi@3.3.0:
|
||||||
resolution: {integrity: sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==}
|
resolution: {integrity: sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==}
|
||||||
|
|
||||||
|
oauth@0.9.15:
|
||||||
|
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
|
||||||
|
|
||||||
object-assign@4.1.1:
|
object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
object-hash@2.2.0:
|
||||||
|
resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
object-inspect@1.13.4:
|
object-inspect@1.13.4:
|
||||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2910,12 +2948,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
oidc-token-hash@5.2.0:
|
||||||
|
resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==}
|
||||||
|
engines: {node: ^10.13.0 || >=12.0.0}
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
one-time@1.0.0:
|
one-time@1.0.0:
|
||||||
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
|
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
|
||||||
|
|
||||||
|
openid-client@5.7.1:
|
||||||
|
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
|
||||||
|
|
||||||
openid-client@6.3.0:
|
openid-client@6.3.0:
|
||||||
resolution: {integrity: sha512-68LMqb/Whtq214B9c9kCtuniCKQrEqWJRTEoOEZlv2QV5VgqhjySCpBe4RXeU+pj/VNOi7erP/ixxfHtqR7FOw==}
|
resolution: {integrity: sha512-68LMqb/Whtq214B9c9kCtuniCKQrEqWJRTEoOEZlv2QV5VgqhjySCpBe4RXeU+pj/VNOi7erP/ixxfHtqR7FOw==}
|
||||||
|
|
||||||
@@ -3007,6 +3052,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
preact-render-to-string@5.2.6:
|
||||||
|
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
|
||||||
|
peerDependencies:
|
||||||
|
preact: '>=10'
|
||||||
|
|
||||||
|
preact@10.28.2:
|
||||||
|
resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==}
|
||||||
|
|
||||||
prelude-ls@1.2.1:
|
prelude-ls@1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -3038,6 +3091,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||||
|
|
||||||
|
pretty-format@3.8.0:
|
||||||
|
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
|
||||||
|
|
||||||
prism-react-renderer@2.4.1:
|
prism-react-renderer@2.4.1:
|
||||||
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
|
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3670,6 +3726,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
uuid@8.3.2:
|
||||||
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
varint@6.0.0:
|
varint@6.0.0:
|
||||||
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
|
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
|
||||||
|
|
||||||
@@ -3868,6 +3928,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
yallist@4.0.0:
|
||||||
|
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||||
|
|
||||||
yallist@5.0.0:
|
yallist@5.0.0:
|
||||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4389,6 +4452,8 @@ snapshots:
|
|||||||
|
|
||||||
'@nolyfill/is-core-module@1.0.39': {}
|
'@nolyfill/is-core-module@1.0.39': {}
|
||||||
|
|
||||||
|
'@panva/hkdf@1.2.1': {}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -5255,6 +5320,8 @@ snapshots:
|
|||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
core-js@3.40.0: {}
|
core-js@3.40.0: {}
|
||||||
|
|
||||||
core-util-is@1.0.3: {}
|
core-util-is@1.0.3: {}
|
||||||
@@ -6347,6 +6414,8 @@ snapshots:
|
|||||||
|
|
||||||
jiti@2.6.1: {}
|
jiti@2.6.1: {}
|
||||||
|
|
||||||
|
jose@4.15.9: {}
|
||||||
|
|
||||||
jose@5.10.0: {}
|
jose@5.10.0: {}
|
||||||
|
|
||||||
js-tokens@10.0.0: {}
|
js-tokens@10.0.0: {}
|
||||||
@@ -6508,6 +6577,10 @@ snapshots:
|
|||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
|
lru-cache@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
yallist: 4.0.0
|
||||||
|
|
||||||
luxon@3.6.1: {}
|
luxon@3.6.1: {}
|
||||||
|
|
||||||
lz-string@1.5.0: {}
|
lz-string@1.5.0: {}
|
||||||
@@ -6587,6 +6660,21 @@ snapshots:
|
|||||||
|
|
||||||
net@1.0.2: {}
|
net@1.0.2: {}
|
||||||
|
|
||||||
|
next-auth@4.24.13(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.6
|
||||||
|
'@panva/hkdf': 1.2.1
|
||||||
|
cookie: 0.7.2
|
||||||
|
jose: 4.15.9
|
||||||
|
next: 15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
oauth: 0.9.15
|
||||||
|
openid-client: 5.7.1
|
||||||
|
preact: 10.28.2
|
||||||
|
preact-render-to-string: 5.2.6(preact@10.28.2)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
uuid: 8.3.2
|
||||||
|
|
||||||
next-i18next@12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
next-i18next@12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.9
|
'@babel/runtime': 7.26.9
|
||||||
@@ -6635,8 +6723,12 @@ snapshots:
|
|||||||
|
|
||||||
oauth4webapi@3.3.0: {}
|
oauth4webapi@3.3.0: {}
|
||||||
|
|
||||||
|
oauth@0.9.15: {}
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
|
object-hash@2.2.0: {}
|
||||||
|
|
||||||
object-inspect@1.13.4: {}
|
object-inspect@1.13.4: {}
|
||||||
|
|
||||||
object-keys@1.1.1: {}
|
object-keys@1.1.1: {}
|
||||||
@@ -6676,6 +6768,8 @@ snapshots:
|
|||||||
define-properties: 1.2.1
|
define-properties: 1.2.1
|
||||||
es-object-atoms: 1.1.1
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
|
oidc-token-hash@5.2.0: {}
|
||||||
|
|
||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
@@ -6684,6 +6778,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fn.name: 1.1.0
|
fn.name: 1.1.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
|
||||||
|
|
||||||
openid-client@6.3.0:
|
openid-client@6.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
jose: 5.10.0
|
jose: 5.10.0
|
||||||
@@ -6766,6 +6867,13 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
preact-render-to-string@5.2.6(preact@10.28.2):
|
||||||
|
dependencies:
|
||||||
|
preact: 10.28.2
|
||||||
|
pretty-format: 3.8.0
|
||||||
|
|
||||||
|
preact@10.28.2: {}
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
prettier-linter-helpers@1.0.0:
|
prettier-linter-helpers@1.0.0:
|
||||||
@@ -6787,6 +6895,8 @@ snapshots:
|
|||||||
ansi-styles: 5.2.0
|
ansi-styles: 5.2.0
|
||||||
react-is: 17.0.2
|
react-is: 17.0.2
|
||||||
|
|
||||||
|
pretty-format@3.8.0: {}
|
||||||
|
|
||||||
prism-react-renderer@2.4.1(react@18.3.1):
|
prism-react-renderer@2.4.1(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/prismjs': 1.26.5
|
'@types/prismjs': 1.26.5
|
||||||
@@ -7544,6 +7654,8 @@ snapshots:
|
|||||||
|
|
||||||
uuid@10.0.0: {}
|
uuid@10.0.0: {}
|
||||||
|
|
||||||
|
uuid@8.3.2: {}
|
||||||
|
|
||||||
varint@6.0.0: {}
|
varint@6.0.0: {}
|
||||||
|
|
||||||
victory-vendor@37.3.6:
|
victory-vendor@37.3.6:
|
||||||
@@ -7780,6 +7892,8 @@ snapshots:
|
|||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
|
yallist@4.0.0: {}
|
||||||
|
|
||||||
yallist@5.0.0: {}
|
yallist@5.0.0: {}
|
||||||
|
|
||||||
yargs-parser@21.1.1: {}
|
yargs-parser@21.1.1: {}
|
||||||
|
|||||||
@@ -1152,11 +1152,11 @@
|
|||||||
"artists": "Kunstenaars"
|
"artists": "Kunstenaars"
|
||||||
},
|
},
|
||||||
"arcane": {
|
"arcane": {
|
||||||
"containers": "Houers",
|
"containers": "Containers",
|
||||||
"images": "Beelde",
|
"images": "Images",
|
||||||
"image_updates": "Beeldopdaterings",
|
"image_updates": "Image Updates",
|
||||||
"images_unused": "Ongebruik",
|
"images_unused": "Unused",
|
||||||
"environment_required": "Omgewings-ID Vereis"
|
"environment_required": "Environment ID Required"
|
||||||
},
|
},
|
||||||
"dockhand": {
|
"dockhand": {
|
||||||
"running": "Lopend",
|
"running": "Lopend",
|
||||||
|
|||||||
@@ -108,14 +108,14 @@
|
|||||||
"songs": "Piosenki"
|
"songs": "Piosenki"
|
||||||
},
|
},
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
"playing": "Odtwarza",
|
"playing": "Playing",
|
||||||
"transcoding": "Transkoduje",
|
"transcoding": "Transcoding",
|
||||||
"bitrate": "Bitrate",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "Brak aktywnych strumieni",
|
"no_active": "No Active Streams",
|
||||||
"movies": "Filmy",
|
"movies": "Movies",
|
||||||
"series": "Seriale",
|
"series": "Series",
|
||||||
"episodes": "Odcinki",
|
"episodes": "Episodes",
|
||||||
"songs": "Piosenki"
|
"songs": "Songs"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
@@ -716,8 +716,8 @@
|
|||||||
"volumeAvailable": "Dostępne"
|
"volumeAvailable": "Dostępne"
|
||||||
},
|
},
|
||||||
"dispatcharr": {
|
"dispatcharr": {
|
||||||
"channels": "Kanały",
|
"channels": "Channels",
|
||||||
"streams": "Strumienie"
|
"streams": "Streams"
|
||||||
},
|
},
|
||||||
"mylar": {
|
"mylar": {
|
||||||
"series": "Seriale",
|
"series": "Seriale",
|
||||||
@@ -1152,11 +1152,11 @@
|
|||||||
"artists": "Wykonawcy"
|
"artists": "Wykonawcy"
|
||||||
},
|
},
|
||||||
"arcane": {
|
"arcane": {
|
||||||
"containers": "Kontenery",
|
"containers": "Containers",
|
||||||
"images": "Obrazy",
|
"images": "Images",
|
||||||
"image_updates": "Aktualizacje obrazów",
|
"image_updates": "Image Updates",
|
||||||
"images_unused": "Nieużywane",
|
"images_unused": "Unused",
|
||||||
"environment_required": "Wymagane ID środowiska"
|
"environment_required": "Environment ID Required"
|
||||||
},
|
},
|
||||||
"dockhand": {
|
"dockhand": {
|
||||||
"running": "Działające",
|
"running": "Działające",
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
"http_status": "HTTP stavový kód",
|
"http_status": "HTTP stavový kód",
|
||||||
"error": "Chyba",
|
"error": "Chyba",
|
||||||
"response": "Odpoveď",
|
"response": "Odpoveď",
|
||||||
"down": "Nedostupné",
|
"down": "Down",
|
||||||
"up": "Beží",
|
"up": "Beží",
|
||||||
"not_available": "Nedostupné"
|
"not_available": "Nedostupné"
|
||||||
},
|
},
|
||||||
@@ -108,18 +108,18 @@
|
|||||||
"songs": "Skladby"
|
"songs": "Skladby"
|
||||||
},
|
},
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
"playing": "Prehráva sa",
|
"playing": "Playing",
|
||||||
"transcoding": "Prebieha prekódovanie",
|
"transcoding": "Transcoding",
|
||||||
"bitrate": "Prenosová rýchlosť",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "Žiadne aktívne vysielania",
|
"no_active": "No Active Streams",
|
||||||
"movies": "Filmov",
|
"movies": "Movies",
|
||||||
"series": "Seriálov",
|
"series": "Series",
|
||||||
"episodes": "Epizód",
|
"episodes": "Episodes",
|
||||||
"songs": "Skladieb"
|
"songs": "Songs"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Nedostupné",
|
"offline": "Offline",
|
||||||
"offline_alt": "Nedostupné",
|
"offline_alt": "Offline",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
"total": "Celkom",
|
"total": "Celkom",
|
||||||
"unknown": "Neznáme"
|
"unknown": "Neznáme"
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
"uptime": "Dostupnosť",
|
"uptime": "Dostupnosť",
|
||||||
"maxDown": "Max. sťahovanie",
|
"maxDown": "Max. sťahovanie",
|
||||||
"maxUp": "Max. nahrávanie",
|
"maxUp": "Max. nahrávanie",
|
||||||
"down": "Nedostupné",
|
"down": "Down",
|
||||||
"up": "Beží",
|
"up": "Beží",
|
||||||
"received": "Prijaté",
|
"received": "Prijaté",
|
||||||
"sent": "Odoslané",
|
"sent": "Odoslané",
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "Playing",
|
"playing": "Playing",
|
||||||
"transcoding": "Transcoding",
|
"transcoding": "Transcoding",
|
||||||
"bitrate": "Prenosová rýchlosť",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "No Active Streams",
|
"no_active": "No Active Streams",
|
||||||
"plex_connection_error": "Skontroluj spojenie s Plex"
|
"plex_connection_error": "Skontroluj spojenie s Plex"
|
||||||
},
|
},
|
||||||
@@ -429,7 +429,7 @@
|
|||||||
"version": "Verzia",
|
"version": "Verzia",
|
||||||
"status": "Stav",
|
"status": "Stav",
|
||||||
"up": "Online",
|
"up": "Online",
|
||||||
"down": "Nedostupné"
|
"down": "Offline"
|
||||||
},
|
},
|
||||||
"miniflux": {
|
"miniflux": {
|
||||||
"read": "Prečítané",
|
"read": "Prečítané",
|
||||||
@@ -450,7 +450,7 @@
|
|||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"load": "Záťaž",
|
"load": "Záťaž",
|
||||||
"wait": "Čakajte, prosím",
|
"wait": "Čakajte, prosím",
|
||||||
"temp": "TEPL",
|
"temp": "TEMP",
|
||||||
"_temp": "Teplota",
|
"_temp": "Teplota",
|
||||||
"warn": "Upozornení",
|
"warn": "Upozornení",
|
||||||
"uptime": "BEŽÍ",
|
"uptime": "BEŽÍ",
|
||||||
@@ -491,13 +491,13 @@
|
|||||||
"51-day": "Mierne mrholenie",
|
"51-day": "Mierne mrholenie",
|
||||||
"51-night": "Slabé mrholenie",
|
"51-night": "Slabé mrholenie",
|
||||||
"53-day": "Mrholenie",
|
"53-day": "Mrholenie",
|
||||||
"53-night": "Mrholenie",
|
"53-night": "Drizzle",
|
||||||
"55-day": "Silné mrholenie",
|
"55-day": "Silné mrholenie",
|
||||||
"55-night": "Silné mrholenie",
|
"55-night": "Silné mrholenie",
|
||||||
"56-day": "Mierne mrazivé mrholenie",
|
"56-day": "Mierne mrazivé mrholenie",
|
||||||
"56-night": "Jemné mrznúce mrholenie",
|
"56-night": "Light Freezing Drizzle",
|
||||||
"57-day": "Mrazivé mrholenie",
|
"57-day": "Mrazivé mrholenie",
|
||||||
"57-night": "Mrznúce mrholenie",
|
"57-night": "Freezing Drizzle",
|
||||||
"61-day": "Slabý dážď",
|
"61-day": "Slabý dážď",
|
||||||
"61-night": "Slabý dážď",
|
"61-night": "Slabý dážď",
|
||||||
"63-day": "Dážď",
|
"63-day": "Dážď",
|
||||||
@@ -542,14 +542,14 @@
|
|||||||
"child_bridges_status": "{{ok}}/{{total}}",
|
"child_bridges_status": "{{ok}}/{{total}}",
|
||||||
"up": "Beží",
|
"up": "Beží",
|
||||||
"pending": "Čakajúce",
|
"pending": "Čakajúce",
|
||||||
"down": "Nedostupné",
|
"down": "Down",
|
||||||
"ok": "Ok"
|
"ok": "Ok"
|
||||||
},
|
},
|
||||||
"healthchecks": {
|
"healthchecks": {
|
||||||
"new": "Nový",
|
"new": "Nový",
|
||||||
"up": "Beží",
|
"up": "Beží",
|
||||||
"grace": "V dodatočnej lehote",
|
"grace": "V dodatočnej lehote",
|
||||||
"down": "Nedostupné",
|
"down": "Down",
|
||||||
"paused": "Pozastavené",
|
"paused": "Pozastavené",
|
||||||
"status": "Stav",
|
"status": "Stav",
|
||||||
"last_ping": "Poslendný ping",
|
"last_ping": "Poslendný ping",
|
||||||
@@ -675,7 +675,7 @@
|
|||||||
"memory": "Využitie pamäte",
|
"memory": "Využitie pamäte",
|
||||||
"wanStatus": "Stav WAN",
|
"wanStatus": "Stav WAN",
|
||||||
"up": "Beží",
|
"up": "Beží",
|
||||||
"down": "Nedostupné",
|
"down": "Down",
|
||||||
"temp": "Temp",
|
"temp": "Temp",
|
||||||
"disk": "Využitie disku",
|
"disk": "Využitie disku",
|
||||||
"wanIP": "IP adresa WAN"
|
"wanIP": "IP adresa WAN"
|
||||||
@@ -776,8 +776,8 @@
|
|||||||
"targets_total": "Cieľov spolu"
|
"targets_total": "Cieľov spolu"
|
||||||
},
|
},
|
||||||
"gatus": {
|
"gatus": {
|
||||||
"up": "Dostupné stránky",
|
"up": "Sites Up",
|
||||||
"down": "Nedostupné stránky",
|
"down": "Sites Down",
|
||||||
"uptime": "Dostupnosť"
|
"uptime": "Dostupnosť"
|
||||||
},
|
},
|
||||||
"ghostfolio": {
|
"ghostfolio": {
|
||||||
@@ -799,7 +799,7 @@
|
|||||||
},
|
},
|
||||||
"whatsupdocker": {
|
"whatsupdocker": {
|
||||||
"monitoring": "Monitoring",
|
"monitoring": "Monitoring",
|
||||||
"updates": "Aktualizácie"
|
"updates": "Updates"
|
||||||
},
|
},
|
||||||
"calibreweb": {
|
"calibreweb": {
|
||||||
"books": "Books",
|
"books": "Books",
|
||||||
@@ -872,7 +872,7 @@
|
|||||||
"uptime": "Dostupnosť",
|
"uptime": "Dostupnosť",
|
||||||
"cpuLoad": "Záťaž CPU priem. (5m)",
|
"cpuLoad": "Záťaž CPU priem. (5m)",
|
||||||
"up": "Beží",
|
"up": "Beží",
|
||||||
"down": "Nedostupné",
|
"down": "Down",
|
||||||
"bytesTx": "Prenesených",
|
"bytesTx": "Prenesených",
|
||||||
"bytesRx": "Prijaté"
|
"bytesRx": "Prijaté"
|
||||||
},
|
},
|
||||||
@@ -881,13 +881,13 @@
|
|||||||
"uptime": "Dostupnosť",
|
"uptime": "Dostupnosť",
|
||||||
"lastDown": "Posledný čas nedostupnosti",
|
"lastDown": "Posledný čas nedostupnosti",
|
||||||
"downDuration": "Trvanie nedostupnosti",
|
"downDuration": "Trvanie nedostupnosti",
|
||||||
"sitesUp": "Dostupné stránky",
|
"sitesUp": "Sites Up",
|
||||||
"sitesDown": "Nedostupné stránky",
|
"sitesDown": "Sites Down",
|
||||||
"paused": "Pozastavené",
|
"paused": "Pozastavené",
|
||||||
"notyetchecked": "Neskontrolované",
|
"notyetchecked": "Neskontrolované",
|
||||||
"up": "Beží",
|
"up": "Beží",
|
||||||
"seemsdown": "Javí sa nedostupný",
|
"seemsdown": "Javí sa nedostupný",
|
||||||
"down": "Nedostupné",
|
"down": "Down",
|
||||||
"unknown": "Neznáme"
|
"unknown": "Neznáme"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
@@ -1023,17 +1023,17 @@
|
|||||||
"last_seen": "Last Seen",
|
"last_seen": "Last Seen",
|
||||||
"status": "Stav",
|
"status": "Stav",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
"offline": "Nedostupné"
|
"offline": "Offline"
|
||||||
},
|
},
|
||||||
"beszel": {
|
"beszel": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"systems": "Systems",
|
"systems": "Systems",
|
||||||
"up": "Beží",
|
"up": "Beží",
|
||||||
"down": "Nedostupné",
|
"down": "Down",
|
||||||
"paused": "Pozastavené",
|
"paused": "Pozastavené",
|
||||||
"pending": "Čakajúce",
|
"pending": "Čakajúce",
|
||||||
"status": "Stav",
|
"status": "Stav",
|
||||||
"updated": "Aktualizované",
|
"updated": "Updated",
|
||||||
"cpu": "CPU",
|
"cpu": "CPU",
|
||||||
"memory": "RAM",
|
"memory": "RAM",
|
||||||
"disk": "Disk",
|
"disk": "Disk",
|
||||||
@@ -1078,7 +1078,7 @@
|
|||||||
"disconnected": "Odpojené",
|
"disconnected": "Odpojené",
|
||||||
"updateStatus": "Update",
|
"updateStatus": "Update",
|
||||||
"update_yes": "Dostupné",
|
"update_yes": "Dostupné",
|
||||||
"update_no": "Aktuálne",
|
"update_no": "Up to Date",
|
||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
"uploads": "Uploads",
|
"uploads": "Uploads",
|
||||||
"sharedFiles": "Files"
|
"sharedFiles": "Files"
|
||||||
@@ -1097,7 +1097,7 @@
|
|||||||
"total": "Celkom",
|
"total": "Celkom",
|
||||||
"running": "Beží",
|
"running": "Beží",
|
||||||
"stopped": "Zastavené",
|
"stopped": "Zastavené",
|
||||||
"down": "Nedostupné",
|
"down": "Down",
|
||||||
"unhealthy": "Nezdravý",
|
"unhealthy": "Nezdravý",
|
||||||
"unknown": "Neznáme",
|
"unknown": "Neznáme",
|
||||||
"servers": "Servery",
|
"servers": "Servery",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": "İşlemci",
|
"cpu": "İşlemci",
|
||||||
"mem": "Bellek",
|
"mem": "MEM",
|
||||||
"total": "Toplam",
|
"total": "Toplam",
|
||||||
"free": "Boş",
|
"free": "Boş",
|
||||||
"used": "Kullanımda",
|
"used": "Kullanımda",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"unhealthy": "Sağlıksız",
|
"unhealthy": "Sağlıksız",
|
||||||
"not_found": "Bulunamadı",
|
"not_found": "Bulunamadı",
|
||||||
"exited": "Kapandı",
|
"exited": "Kapandı",
|
||||||
"partial": "Kısmi"
|
"partial": "Parçalı"
|
||||||
},
|
},
|
||||||
"ping": {
|
"ping": {
|
||||||
"error": "Hata",
|
"error": "Hata",
|
||||||
@@ -93,14 +93,14 @@
|
|||||||
"http_status": "HTTPS durumu",
|
"http_status": "HTTPS durumu",
|
||||||
"error": "Hata",
|
"error": "Hata",
|
||||||
"response": "Yanıt",
|
"response": "Yanıt",
|
||||||
"down": "İndirme",
|
"down": "Çalışmayan",
|
||||||
"up": "Çalışıyor",
|
"up": "Çalışıyor",
|
||||||
"not_available": "Uygun değil"
|
"not_available": "Uygun değil"
|
||||||
},
|
},
|
||||||
"emby": {
|
"emby": {
|
||||||
"playing": "Oynatılıyor",
|
"playing": "Oynatılıyor",
|
||||||
"transcoding": "Dönüştürülüyor",
|
"transcoding": "Dönüştürülüyor",
|
||||||
"bitrate": "Bit Hızı",
|
"bitrate": "Bit Oranı",
|
||||||
"no_active": "Etkin akış yok",
|
"no_active": "Etkin akış yok",
|
||||||
"movies": "Filmler",
|
"movies": "Filmler",
|
||||||
"series": "Diziler",
|
"series": "Diziler",
|
||||||
@@ -108,14 +108,14 @@
|
|||||||
"songs": "Şarkılar"
|
"songs": "Şarkılar"
|
||||||
},
|
},
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
"playing": "Oynatılıyor",
|
"playing": "Playing",
|
||||||
"transcoding": "Dönüştürülüyor",
|
"transcoding": "Transcoding",
|
||||||
"bitrate": "Bit Hızı",
|
"bitrate": "Bitrate",
|
||||||
"no_active": "Aktif Yayın Yok",
|
"no_active": "No Active Streams",
|
||||||
"movies": "Filmler",
|
"movies": "Movies",
|
||||||
"series": "Diziler",
|
"series": "Series",
|
||||||
"episodes": "Bölümler",
|
"episodes": "Episodes",
|
||||||
"songs": "Şarkılar"
|
"songs": "Songs"
|
||||||
},
|
},
|
||||||
"esphome": {
|
"esphome": {
|
||||||
"offline": "Çevrimdışı",
|
"offline": "Çevrimdışı",
|
||||||
@@ -135,8 +135,8 @@
|
|||||||
"flood": {
|
"flood": {
|
||||||
"download": "İndirme",
|
"download": "İndirme",
|
||||||
"upload": "Yükleme",
|
"upload": "Yükleme",
|
||||||
"leech": "İndirilen",
|
"leech": "Tüketici",
|
||||||
"seed": "Gönderilen"
|
"seed": "Sağlayıcı"
|
||||||
},
|
},
|
||||||
"freshrss": {
|
"freshrss": {
|
||||||
"subscriptions": "Abonelikler",
|
"subscriptions": "Abonelikler",
|
||||||
@@ -152,10 +152,10 @@
|
|||||||
"connectionStatusDisconnected": "Bağlı değil",
|
"connectionStatusDisconnected": "Bağlı değil",
|
||||||
"connectionStatusConnected": "Bağlı",
|
"connectionStatusConnected": "Bağlı",
|
||||||
"uptime": "Çalışma Süresi",
|
"uptime": "Çalışma Süresi",
|
||||||
"maxDown": "Maks. İndirme",
|
"maxDown": "Max. Indirme",
|
||||||
"maxUp": "Maks. Gönderme",
|
"maxUp": "Max. Gönderme",
|
||||||
"down": "İndirme",
|
"down": "Çalışmayan",
|
||||||
"up": "Yükleme",
|
"up": "Çalışıyor",
|
||||||
"received": "Alınan",
|
"received": "Alınan",
|
||||||
"sent": "Gönderilen",
|
"sent": "Gönderilen",
|
||||||
"externalIPAddress": "Harici IP",
|
"externalIPAddress": "Harici IP",
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
"tautulli": {
|
"tautulli": {
|
||||||
"playing": "Oynatılıyor",
|
"playing": "Oynatılıyor",
|
||||||
"transcoding": "Dönüştürülüyor",
|
"transcoding": "Dönüştürülüyor",
|
||||||
"bitrate": "Bit Hızı",
|
"bitrate": "Bit Oranı",
|
||||||
"no_active": "Etkin akış yok",
|
"no_active": "Etkin akış yok",
|
||||||
"plex_connection_error": "Plex Bağlantısı Kontrol Ediliyor"
|
"plex_connection_error": "Plex Bağlantısı Kontrol Ediliyor"
|
||||||
},
|
},
|
||||||
@@ -209,20 +209,20 @@
|
|||||||
},
|
},
|
||||||
"rutorrent": {
|
"rutorrent": {
|
||||||
"active": "Etkin",
|
"active": "Etkin",
|
||||||
"upload": "Gönderme",
|
"upload": "Yükleme",
|
||||||
"download": "İndirme"
|
"download": "İndirme"
|
||||||
},
|
},
|
||||||
"transmission": {
|
"transmission": {
|
||||||
"download": "İndirme",
|
"download": "İndirme",
|
||||||
"upload": "Gönderme",
|
"upload": "Yükleme",
|
||||||
"leech": "İndirilen",
|
"leech": "Tüketici",
|
||||||
"seed": "Gönderilen"
|
"seed": "Sağlayıcı"
|
||||||
},
|
},
|
||||||
"qbittorrent": {
|
"qbittorrent": {
|
||||||
"download": "İndirme",
|
"download": "İndirme",
|
||||||
"upload": "Gönderme",
|
"upload": "Yükleme",
|
||||||
"leech": "İndirilen",
|
"leech": "Tüketici",
|
||||||
"seed": "Gönderilen"
|
"seed": "Sağlayıcı"
|
||||||
},
|
},
|
||||||
"qnap": {
|
"qnap": {
|
||||||
"cpuUsage": "İşlemci Kullanımı",
|
"cpuUsage": "İşlemci Kullanımı",
|
||||||
@@ -234,9 +234,9 @@
|
|||||||
},
|
},
|
||||||
"deluge": {
|
"deluge": {
|
||||||
"download": "İndirme",
|
"download": "İndirme",
|
||||||
"upload": "Gönderme",
|
"upload": "Yükleme",
|
||||||
"leech": "İndirilen",
|
"leech": "Leech",
|
||||||
"seed": "Gönderilen"
|
"seed": "Seed"
|
||||||
},
|
},
|
||||||
"develancacheui": {
|
"develancacheui": {
|
||||||
"cachehitbytes": "Önbellek İsabetli Byte",
|
"cachehitbytes": "Önbellek İsabetli Byte",
|
||||||
@@ -244,14 +244,14 @@
|
|||||||
},
|
},
|
||||||
"downloadstation": {
|
"downloadstation": {
|
||||||
"download": "İndirme",
|
"download": "İndirme",
|
||||||
"upload": "Gönderme",
|
"upload": "Yükleme",
|
||||||
"leech": "İndirilen",
|
"leech": "Tüketici",
|
||||||
"seed": "Gönderilen"
|
"seed": "Sağlayıcı"
|
||||||
},
|
},
|
||||||
"sonarr": {
|
"sonarr": {
|
||||||
"wanted": "İstendi",
|
"wanted": "İstendi",
|
||||||
"queued": "Kuyrukta",
|
"queued": "Kuyrukta",
|
||||||
"series": "Diziler",
|
"series": "Seriler",
|
||||||
"queue": "Kuyruk",
|
"queue": "Kuyruk",
|
||||||
"unknown": "Bilinmeyen"
|
"unknown": "Bilinmeyen"
|
||||||
},
|
},
|
||||||
@@ -286,10 +286,10 @@
|
|||||||
"pending": "Bekleyen",
|
"pending": "Bekleyen",
|
||||||
"approved": "Onaylı",
|
"approved": "Onaylı",
|
||||||
"available": "Uygun",
|
"available": "Uygun",
|
||||||
"issues": "Açık Sorunlar"
|
"issues": "Open Issues"
|
||||||
},
|
},
|
||||||
"overseerr": {
|
"overseerr": {
|
||||||
"pending": "Beklemede",
|
"pending": "Pending",
|
||||||
"processing": "İşleniyor",
|
"processing": "İşleniyor",
|
||||||
"approved": "Onaylı",
|
"approved": "Onaylı",
|
||||||
"available": "Uygun"
|
"available": "Uygun"
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
"gravity": "Gravity"
|
"gravity": "Gravity"
|
||||||
},
|
},
|
||||||
"adguard": {
|
"adguard": {
|
||||||
"queries": "Sorgular",
|
"queries": "Queries",
|
||||||
"blocked": "Engellenen",
|
"blocked": "Engellenen",
|
||||||
"filtered": "Filtrelendi",
|
"filtered": "Filtrelendi",
|
||||||
"latency": "Gecikme"
|
"latency": "Gecikme"
|
||||||
@@ -448,9 +448,9 @@
|
|||||||
},
|
},
|
||||||
"glances": {
|
"glances": {
|
||||||
"cpu": "İşlemci",
|
"cpu": "İşlemci",
|
||||||
"load": "Yük",
|
"load": "Load",
|
||||||
"wait": "Lütfen bekleyin",
|
"wait": "Lütfen bekleyin",
|
||||||
"temp": "Sıcaklık",
|
"temp": "TEMP",
|
||||||
"_temp": "Sıcaklık",
|
"_temp": "Sıcaklık",
|
||||||
"warn": "Uyarı",
|
"warn": "Uyarı",
|
||||||
"uptime": "ÇALIŞIYOR",
|
"uptime": "ÇALIŞIYOR",
|
||||||
@@ -463,7 +463,7 @@
|
|||||||
"read": "Okundu",
|
"read": "Okundu",
|
||||||
"write": "Yazma",
|
"write": "Yazma",
|
||||||
"gpu": "GPU",
|
"gpu": "GPU",
|
||||||
"mem": "Bellek",
|
"mem": "Hafıza",
|
||||||
"swap": "Swap"
|
"swap": "Swap"
|
||||||
},
|
},
|
||||||
"quicklaunch": {
|
"quicklaunch": {
|
||||||
@@ -543,7 +543,7 @@
|
|||||||
"up": "Çalışıyor",
|
"up": "Çalışıyor",
|
||||||
"pending": "Bekleyen",
|
"pending": "Bekleyen",
|
||||||
"down": "Çalışmayan",
|
"down": "Çalışmayan",
|
||||||
"ok": "Tamam"
|
"ok": "Ok"
|
||||||
},
|
},
|
||||||
"healthchecks": {
|
"healthchecks": {
|
||||||
"new": "Yeni",
|
"new": "Yeni",
|
||||||
@@ -598,7 +598,7 @@
|
|||||||
"signalStrength": "Sağlamlık",
|
"signalStrength": "Sağlamlık",
|
||||||
"signalQuality": "Kalite",
|
"signalQuality": "Kalite",
|
||||||
"symbolQuality": "Kalite",
|
"symbolQuality": "Kalite",
|
||||||
"networkRate": "Bit Hızı",
|
"networkRate": "Bit Oranı",
|
||||||
"clientIP": "Alıcı"
|
"clientIP": "Alıcı"
|
||||||
},
|
},
|
||||||
"scrutiny": {
|
"scrutiny": {
|
||||||
@@ -611,13 +611,13 @@
|
|||||||
"total": "Toplam"
|
"total": "Toplam"
|
||||||
},
|
},
|
||||||
"pangolin": {
|
"pangolin": {
|
||||||
"orgs": "Kuruluşlar",
|
"orgs": "Orgs",
|
||||||
"sites": "Siteler",
|
"sites": "Sites",
|
||||||
"resources": "Kaynaklar",
|
"resources": "Resources",
|
||||||
"targets": "Hedefler",
|
"targets": "Targets",
|
||||||
"traffic": "Trafik",
|
"traffic": "Traffic",
|
||||||
"in": "Gelen",
|
"in": "In",
|
||||||
"out": "Giden"
|
"out": "Out"
|
||||||
},
|
},
|
||||||
"peanut": {
|
"peanut": {
|
||||||
"battery_charge": "Pil Yüzdesi",
|
"battery_charge": "Pil Yüzdesi",
|
||||||
@@ -676,7 +676,7 @@
|
|||||||
"wanStatus": "WAN Durumu",
|
"wanStatus": "WAN Durumu",
|
||||||
"up": "Çalışıyor",
|
"up": "Çalışıyor",
|
||||||
"down": "Çalışmayan",
|
"down": "Çalışmayan",
|
||||||
"temp": "Sıcaklık",
|
"temp": "Temp",
|
||||||
"disk": "Disk Kullanımı",
|
"disk": "Disk Kullanımı",
|
||||||
"wanIP": "WAN IP"
|
"wanIP": "WAN IP"
|
||||||
},
|
},
|
||||||
@@ -697,7 +697,7 @@
|
|||||||
"down": "Çalışmayan site",
|
"down": "Çalışmayan site",
|
||||||
"uptime": "Çalışma süresi",
|
"uptime": "Çalışma süresi",
|
||||||
"incident": "Olay",
|
"incident": "Olay",
|
||||||
"m": "dk"
|
"m": "m"
|
||||||
},
|
},
|
||||||
"atsumeru": {
|
"atsumeru": {
|
||||||
"series": "Diziler",
|
"series": "Diziler",
|
||||||
@@ -716,8 +716,8 @@
|
|||||||
"volumeAvailable": "Uygun"
|
"volumeAvailable": "Uygun"
|
||||||
},
|
},
|
||||||
"dispatcharr": {
|
"dispatcharr": {
|
||||||
"channels": "Kanallar",
|
"channels": "Channels",
|
||||||
"streams": "Akışlar"
|
"streams": "Streams"
|
||||||
},
|
},
|
||||||
"mylar": {
|
"mylar": {
|
||||||
"series": "Diziler",
|
"series": "Diziler",
|
||||||
@@ -771,12 +771,12 @@
|
|||||||
"nodes": "Düğümler"
|
"nodes": "Düğümler"
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"targets_up": "Çalışan Hedef",
|
"targets_up": "Hedef Çalışıyor",
|
||||||
"targets_down": "Çalışmayan hedef",
|
"targets_down": "Çalışmayan hedef",
|
||||||
"targets_total": "Toplam Hedef"
|
"targets_total": "Toplam Hedef"
|
||||||
},
|
},
|
||||||
"gatus": {
|
"gatus": {
|
||||||
"up": "Çalışan Siteler",
|
"up": "Sites Up",
|
||||||
"down": "Çalışmayan site",
|
"down": "Çalışmayan site",
|
||||||
"uptime": "Çalışma süresi"
|
"uptime": "Çalışma süresi"
|
||||||
},
|
},
|
||||||
@@ -784,7 +784,7 @@
|
|||||||
"gross_percent_today": "Bugün",
|
"gross_percent_today": "Bugün",
|
||||||
"gross_percent_1y": "Bir yıl",
|
"gross_percent_1y": "Bir yıl",
|
||||||
"gross_percent_max": "Tüm zaman",
|
"gross_percent_max": "Tüm zaman",
|
||||||
"net_worth": "Net Değer"
|
"net_worth": "Net Worth"
|
||||||
},
|
},
|
||||||
"audiobookshelf": {
|
"audiobookshelf": {
|
||||||
"podcasts": "Podcast",
|
"podcasts": "Podcast",
|
||||||
@@ -805,13 +805,13 @@
|
|||||||
"books": "Kitaplar",
|
"books": "Kitaplar",
|
||||||
"authors": "Yazarlar",
|
"authors": "Yazarlar",
|
||||||
"categories": "Kategoriler",
|
"categories": "Kategoriler",
|
||||||
"series": "Diziler"
|
"series": "Seriler"
|
||||||
},
|
},
|
||||||
"booklore": {
|
"booklore": {
|
||||||
"libraries": "Kütüphaneler",
|
"libraries": "Libraries",
|
||||||
"books": "Kitaplar",
|
"books": "Books",
|
||||||
"reading": "Okunuyor",
|
"reading": "Reading",
|
||||||
"finished": "Bitti"
|
"finished": "Finished"
|
||||||
},
|
},
|
||||||
"jdownloader": {
|
"jdownloader": {
|
||||||
"downloadCount": "Kuyruk",
|
"downloadCount": "Kuyruk",
|
||||||
@@ -820,7 +820,7 @@
|
|||||||
"downloadSpeed": "Hız"
|
"downloadSpeed": "Hız"
|
||||||
},
|
},
|
||||||
"kavita": {
|
"kavita": {
|
||||||
"seriesCount": "Diziler",
|
"seriesCount": "Seriler",
|
||||||
"totalFiles": "Dosyalar"
|
"totalFiles": "Dosyalar"
|
||||||
},
|
},
|
||||||
"azuredevops": {
|
"azuredevops": {
|
||||||
@@ -865,7 +865,7 @@
|
|||||||
"total": "Toplam",
|
"total": "Toplam",
|
||||||
"running": "Çalışıyor",
|
"running": "Çalışıyor",
|
||||||
"stopped": "Durdu",
|
"stopped": "Durdu",
|
||||||
"passed": "Başarılı",
|
"passed": "Passed",
|
||||||
"failed": "Başarısız"
|
"failed": "Başarısız"
|
||||||
},
|
},
|
||||||
"openwrt": {
|
"openwrt": {
|
||||||
@@ -874,7 +874,7 @@
|
|||||||
"up": "Çalışıyor",
|
"up": "Çalışıyor",
|
||||||
"down": "Çalışmayan",
|
"down": "Çalışmayan",
|
||||||
"bytesTx": "İletilen",
|
"bytesTx": "İletilen",
|
||||||
"bytesRx": "Alınan"
|
"bytesRx": "Received"
|
||||||
},
|
},
|
||||||
"uptimerobot": {
|
"uptimerobot": {
|
||||||
"status": "Durum",
|
"status": "Durum",
|
||||||
@@ -924,7 +924,7 @@
|
|||||||
},
|
},
|
||||||
"gitea": {
|
"gitea": {
|
||||||
"notifications": "Bildirimler",
|
"notifications": "Bildirimler",
|
||||||
"issues": "Sorunlar",
|
"issues": "Issues",
|
||||||
"pulls": "Değişiklik İstekleri",
|
"pulls": "Değişiklik İstekleri",
|
||||||
"repositories": "Depolar"
|
"repositories": "Depolar"
|
||||||
},
|
},
|
||||||
@@ -1006,21 +1006,21 @@
|
|||||||
"lubelogger": {
|
"lubelogger": {
|
||||||
"vehicle": "Taşıt",
|
"vehicle": "Taşıt",
|
||||||
"vehicles": "Taşıtlar",
|
"vehicles": "Taşıtlar",
|
||||||
"serviceRecords": "Servis Kayıtları",
|
"serviceRecords": "Service Records",
|
||||||
"reminders": "Hatırlatıcılar",
|
"reminders": "Hatırlatıcılar",
|
||||||
"nextReminder": "Sonraki hatırlatıcı",
|
"nextReminder": "Sonraki hatırlatıcı",
|
||||||
"none": "Hiçbiri"
|
"none": "None"
|
||||||
},
|
},
|
||||||
"vikunja": {
|
"vikunja": {
|
||||||
"projects": "Etkin projeler",
|
"projects": "Etkin projeler",
|
||||||
"tasks7d": "Bitişi Bu Hafta Olan Görevler",
|
"tasks7d": "Bitişi Bu Hafta Olan Görevler",
|
||||||
"tasksOverdue": "Gecikmiş Görevler",
|
"tasksOverdue": "Overdue Tasks",
|
||||||
"tasksInProgress": "Devam Eden Görevler"
|
"tasksInProgress": "Tasks In Progress"
|
||||||
},
|
},
|
||||||
"headscale": {
|
"headscale": {
|
||||||
"name": "Ad",
|
"name": "Ad",
|
||||||
"address": "Adres",
|
"address": "Adres",
|
||||||
"last_seen": "Son Görülme",
|
"last_seen": "Last Seen",
|
||||||
"status": "Durum",
|
"status": "Durum",
|
||||||
"online": "Çevrimiçi",
|
"online": "Çevrimiçi",
|
||||||
"offline": "Çevrimdışı"
|
"offline": "Çevrimdışı"
|
||||||
@@ -1031,21 +1031,21 @@
|
|||||||
"up": "Çalışıyor",
|
"up": "Çalışıyor",
|
||||||
"down": "Çalışmayan",
|
"down": "Çalışmayan",
|
||||||
"paused": "Durduruldu",
|
"paused": "Durduruldu",
|
||||||
"pending": "Beklemede",
|
"pending": "Pending",
|
||||||
"status": "Durum",
|
"status": "Durum",
|
||||||
"updated": "Güncellendi",
|
"updated": "Güncellendi",
|
||||||
"cpu": "İşlemci",
|
"cpu": "İşlemci",
|
||||||
"memory": "Bellek",
|
"memory": "Bellek",
|
||||||
"disk": "Depolama",
|
"disk": "Disk",
|
||||||
"network": "NET"
|
"network": "NET"
|
||||||
},
|
},
|
||||||
"argocd": {
|
"argocd": {
|
||||||
"apps": "Uygulamalar",
|
"apps": "Uygulamalar",
|
||||||
"synced": "Senkron",
|
"synced": "Synced",
|
||||||
"outOfSync": "Senkron Değil",
|
"outOfSync": "Out Of Sync",
|
||||||
"healthy": "Sağlıklı",
|
"healthy": "Sağlıklı",
|
||||||
"degraded": "Sorunlu",
|
"degraded": "Degraded",
|
||||||
"progressing": "Uygulanıyor",
|
"progressing": "Progressing",
|
||||||
"missing": "Eksik",
|
"missing": "Eksik",
|
||||||
"suspended": "Askıya Alındı"
|
"suspended": "Askıya Alındı"
|
||||||
},
|
},
|
||||||
@@ -1053,22 +1053,22 @@
|
|||||||
"loading": "Yükleniyor"
|
"loading": "Yükleniyor"
|
||||||
},
|
},
|
||||||
"gitlab": {
|
"gitlab": {
|
||||||
"groups": "Gruplar",
|
"groups": "Groups",
|
||||||
"issues": "Sorunlar",
|
"issues": "Issues",
|
||||||
"merges": "Birleştirme İstekleri",
|
"merges": "Merge Requests",
|
||||||
"projects": "Projeler"
|
"projects": "Projects"
|
||||||
},
|
},
|
||||||
"apcups": {
|
"apcups": {
|
||||||
"status": "Durum",
|
"status": "Durum",
|
||||||
"load": "Yük",
|
"load": "Load",
|
||||||
"bcharge": "Pil Yüzdesi",
|
"bcharge": "Battery Charge",
|
||||||
"timeleft": "Kalan zaman"
|
"timeleft": "Kalan zaman"
|
||||||
},
|
},
|
||||||
"karakeep": {
|
"karakeep": {
|
||||||
"bookmarks": "Yer imleri",
|
"bookmarks": "Yer imleri",
|
||||||
"favorites": "Gözdeler",
|
"favorites": "Gözdeler",
|
||||||
"archived": "Arşivlenen",
|
"archived": "Archived",
|
||||||
"highlights": "Öne Çıkanlar",
|
"highlights": "Highlights",
|
||||||
"lists": "Listeler",
|
"lists": "Listeler",
|
||||||
"tags": "Etiketler"
|
"tags": "Etiketler"
|
||||||
},
|
},
|
||||||
@@ -1090,8 +1090,8 @@
|
|||||||
"other": "Diğer"
|
"other": "Diğer"
|
||||||
},
|
},
|
||||||
"checkmk": {
|
"checkmk": {
|
||||||
"serviceErrors": "Hizmet Sorunları",
|
"serviceErrors": "Service issues",
|
||||||
"hostErrors": "Sunucu Sorunları"
|
"hostErrors": "Host issues"
|
||||||
},
|
},
|
||||||
"komodo": {
|
"komodo": {
|
||||||
"total": "Toplam",
|
"total": "Toplam",
|
||||||
@@ -1101,8 +1101,8 @@
|
|||||||
"unhealthy": "Sağlıksız",
|
"unhealthy": "Sağlıksız",
|
||||||
"unknown": "Bilinmeyen",
|
"unknown": "Bilinmeyen",
|
||||||
"servers": "Sunucular",
|
"servers": "Sunucular",
|
||||||
"stacks": "Yığınlar",
|
"stacks": "Stacks",
|
||||||
"containers": "Konteynerler"
|
"containers": "Containers"
|
||||||
},
|
},
|
||||||
"filebrowser": {
|
"filebrowser": {
|
||||||
"available": "Uygun",
|
"available": "Uygun",
|
||||||
@@ -1120,11 +1120,11 @@
|
|||||||
"STARTED": "Başladı",
|
"STARTED": "Başladı",
|
||||||
"STOPPED": "Durdu",
|
"STOPPED": "Durdu",
|
||||||
"NEW_ARRAY": "Yeni dizi",
|
"NEW_ARRAY": "Yeni dizi",
|
||||||
"RECON_DISK": "Disk Yeniden Oluşturuluyor",
|
"RECON_DISK": "Reconstructing Disk",
|
||||||
"DISABLE_DISK": "Disk devre dışı",
|
"DISABLE_DISK": "Disk devre dışı",
|
||||||
"SWAP_DSBL": "Swap devre dışı",
|
"SWAP_DSBL": "Swap devre dışı",
|
||||||
"INVALID_EXPANSION": "Geçersiz Genişletme",
|
"INVALID_EXPANSION": "Invalid Expansion",
|
||||||
"PARITY_NOT_BIGGEST": "Parity En Büyük Disk Değil",
|
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
|
||||||
"TOO_MANY_MISSING_DISKS": "Çok fazla disk eksik",
|
"TOO_MANY_MISSING_DISKS": "Çok fazla disk eksik",
|
||||||
"NEW_DISK_TOO_SMALL": "Yeni disk çok küçük",
|
"NEW_DISK_TOO_SMALL": "Yeni disk çok küçük",
|
||||||
"NO_DATA_DISKS": "Veri diski yok",
|
"NO_DATA_DISKS": "Veri diski yok",
|
||||||
@@ -1139,37 +1139,37 @@
|
|||||||
"poolFree": "{{pool}} boş"
|
"poolFree": "{{pool}} boş"
|
||||||
},
|
},
|
||||||
"backrest": {
|
"backrest": {
|
||||||
"num_plans": "Planlar",
|
"num_plans": "Plans",
|
||||||
"num_success_30": "Başarılılar",
|
"num_success_30": "Successes",
|
||||||
"num_failure_30": "Başarısızlıklar",
|
"num_failure_30": "Failures",
|
||||||
"num_success_latest": "Başarılı",
|
"num_success_latest": "Succeeding",
|
||||||
"num_failure_latest": "Başarısız",
|
"num_failure_latest": "Failing",
|
||||||
"bytes_added_30": "Eklenen Veri"
|
"bytes_added_30": "Bytes Added"
|
||||||
},
|
},
|
||||||
"yourspotify": {
|
"yourspotify": {
|
||||||
"songs": "Şarkılar",
|
"songs": "Songs",
|
||||||
"time": "Zaman",
|
"time": "Time",
|
||||||
"artists": "Sanatçılar"
|
"artists": "Artists"
|
||||||
},
|
},
|
||||||
"arcane": {
|
"arcane": {
|
||||||
"containers": "Konteynerler",
|
"containers": "Containers",
|
||||||
"images": "İmajlar",
|
"images": "Images",
|
||||||
"image_updates": "İmaj Güncellemeleri",
|
"image_updates": "Image Updates",
|
||||||
"images_unused": "Kullanılmayan İmajlar",
|
"images_unused": "Unused",
|
||||||
"environment_required": "Ortam Kimliği Gerekli"
|
"environment_required": "Environment ID Required"
|
||||||
},
|
},
|
||||||
"dockhand": {
|
"dockhand": {
|
||||||
"running": "Çalışan",
|
"running": "Running",
|
||||||
"stopped": "Durdurulan",
|
"stopped": "Stopped",
|
||||||
"cpu": "İşlemci",
|
"cpu": "CPU",
|
||||||
"memory": "Bellek",
|
"memory": "Memory",
|
||||||
"images": "İmajlar",
|
"images": "Images",
|
||||||
"volumes": "Birimler",
|
"volumes": "Volumes",
|
||||||
"events_today": "Bugünkü Olaylar",
|
"events_today": "Events Today",
|
||||||
"pending_updates": "Bekleyen Güncellemeler",
|
"pending_updates": "Pending Updates",
|
||||||
"stacks": "Yığınlar",
|
"stacks": "Stacks",
|
||||||
"paused": "Duraklatılan",
|
"paused": "Paused",
|
||||||
"total": "Toplam",
|
"total": "Total",
|
||||||
"environment_not_found": "Ortam Bulunamadı"
|
"environment_not_found": "Environment Not Found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1059,10 +1059,10 @@
|
|||||||
"projects": "项目"
|
"projects": "项目"
|
||||||
},
|
},
|
||||||
"apcups": {
|
"apcups": {
|
||||||
"status": "状态",
|
"status": "Status",
|
||||||
"load": "负载",
|
"load": "Load",
|
||||||
"bcharge": "电池电量",
|
"bcharge": "Battery Charge",
|
||||||
"timeleft": "剩余供电时间"
|
"timeleft": "Time Left"
|
||||||
},
|
},
|
||||||
"karakeep": {
|
"karakeep": {
|
||||||
"bookmarks": "书签",
|
"bookmarks": "书签",
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "homepage-docs"
|
|
||||||
version = "1.0.0"
|
|
||||||
description = "Documentation for the Homepage project"
|
|
||||||
requires-python = ">=3.13"
|
|
||||||
dependencies = [
|
|
||||||
"zensical>=0.0.21",
|
|
||||||
]
|
|
||||||
47
requirements.txt
Normal file
47
requirements.txt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
Babel==2.12.1
|
||||||
|
backrefs==5.9
|
||||||
|
cairocffi==1.7.1
|
||||||
|
CairoSVG==2.7.1
|
||||||
|
certifi==2023.7.22
|
||||||
|
cffi==1.17.1
|
||||||
|
cfgv==3.4.0
|
||||||
|
charset-normalizer==3.2.0
|
||||||
|
click==8.1.7
|
||||||
|
colorama==0.4.6
|
||||||
|
cssselect2==0.7.0
|
||||||
|
defusedxml==0.7.1
|
||||||
|
distlib==0.3.9
|
||||||
|
filelock==3.17.0
|
||||||
|
ghp-import==2.1.0
|
||||||
|
identify==2.6.7
|
||||||
|
idna==3.4
|
||||||
|
Jinja2==3.1.2
|
||||||
|
Markdown==3.4.4
|
||||||
|
MarkupSafe==2.1.3
|
||||||
|
mergedeep==1.3.4
|
||||||
|
mkdocs==1.6.0
|
||||||
|
mkdocs-get-deps==0.2.0
|
||||||
|
mkdocs-material==9.6.18
|
||||||
|
mkdocs-material-extensions==1.3.1
|
||||||
|
mkdocs-redirects==1.2.1
|
||||||
|
nodeenv==1.9.1
|
||||||
|
packaging==23.1
|
||||||
|
paginate==0.5.6
|
||||||
|
pathspec==0.11.2
|
||||||
|
pillow==10.4.0
|
||||||
|
platformdirs==3.10.0
|
||||||
|
pre-commit==3.5.0
|
||||||
|
pycparser==2.22
|
||||||
|
Pygments==2.16.1
|
||||||
|
pymdown-extensions==10.3
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
PyYAML==6.0.1
|
||||||
|
pyyaml_env_tag==0.1
|
||||||
|
regex==2023.8.8
|
||||||
|
requests==2.31.0
|
||||||
|
six==1.16.0
|
||||||
|
tinycss2==1.4.0
|
||||||
|
urllib3==2.0.5
|
||||||
|
virtualenv==20.29.2
|
||||||
|
watchdog==3.0.0
|
||||||
|
webencodings==0.5.1
|
||||||
124
src/__tests__/pages/api/auth/[...nextauth].test.js
Normal file
124
src/__tests__/pages/api/auth/[...nextauth].test.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const { nextAuthMock } = vi.hoisted(() => ({
|
||||||
|
nextAuthMock: vi.fn((options) => ({ options })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("next-auth", () => ({
|
||||||
|
default: nextAuthMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("pages/api/auth/[...nextauth]", () => {
|
||||||
|
const originalEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
nextAuthMock.mockClear();
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
delete process.env.NEXTAUTH_SECRET;
|
||||||
|
delete process.env.NEXTAUTH_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("configures no providers when auth is disabled", async () => {
|
||||||
|
const mod = await import("pages/api/auth/[...nextauth]");
|
||||||
|
|
||||||
|
expect(nextAuthMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mod.default.options.providers).toEqual([]);
|
||||||
|
expect(mod.default.options.pages?.signIn).toBe("/auth/signin");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps HOMEPAGE_AUTH_SECRET and HOMEPAGE_EXTERNAL_URL to NextAuth envs", async () => {
|
||||||
|
process.env.HOMEPAGE_AUTH_SECRET = "secret";
|
||||||
|
process.env.HOMEPAGE_EXTERNAL_URL = "https://homepage.example";
|
||||||
|
|
||||||
|
const mod = await import("pages/api/auth/[...nextauth]");
|
||||||
|
|
||||||
|
expect(process.env.NEXTAUTH_SECRET).toBe("secret");
|
||||||
|
expect(process.env.NEXTAUTH_URL).toBe("https://homepage.example");
|
||||||
|
expect(mod.default.options.secret).toBe("secret");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when auth is enabled but no provider settings are present", async () => {
|
||||||
|
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
||||||
|
|
||||||
|
await expect(import("pages/api/auth/[...nextauth]")).rejects.toThrow(
|
||||||
|
/Password auth is enabled but required settings are missing/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds a password provider when auth is enabled without OIDC config", async () => {
|
||||||
|
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
||||||
|
process.env.HOMEPAGE_AUTH_PASSWORD = "secret";
|
||||||
|
process.env.HOMEPAGE_AUTH_SECRET = "auth-secret";
|
||||||
|
|
||||||
|
const mod = await import("pages/api/auth/[...nextauth]");
|
||||||
|
const [provider] = mod.default.options.providers;
|
||||||
|
|
||||||
|
expect(provider.id).toBe("credentials");
|
||||||
|
expect(provider.name).toBe("Credentials");
|
||||||
|
expect(provider.type).toBe("credentials");
|
||||||
|
expect(typeof provider.authorize).toBe("function");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds an OIDC provider when enabled and maps profile fields", async () => {
|
||||||
|
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
||||||
|
process.env.HOMEPAGE_OIDC_ISSUER = "https://issuer.example/";
|
||||||
|
process.env.HOMEPAGE_OIDC_CLIENT_ID = "client-id";
|
||||||
|
process.env.HOMEPAGE_OIDC_CLIENT_SECRET = "client-secret";
|
||||||
|
process.env.HOMEPAGE_AUTH_SECRET = "auth-secret";
|
||||||
|
process.env.HOMEPAGE_EXTERNAL_URL = "https://homepage.example";
|
||||||
|
process.env.HOMEPAGE_OIDC_NAME = "My OIDC";
|
||||||
|
process.env.HOMEPAGE_OIDC_SCOPE = "openid email";
|
||||||
|
|
||||||
|
const mod = await import("pages/api/auth/[...nextauth]");
|
||||||
|
const [provider] = mod.default.options.providers;
|
||||||
|
|
||||||
|
expect(provider).toMatchObject({
|
||||||
|
id: "homepage-oidc",
|
||||||
|
name: "My OIDC",
|
||||||
|
type: "oauth",
|
||||||
|
idToken: true,
|
||||||
|
issuer: "https://issuer.example",
|
||||||
|
wellKnown: "https://issuer.example/.well-known/openid-configuration",
|
||||||
|
clientId: "client-id",
|
||||||
|
clientSecret: "client-secret",
|
||||||
|
});
|
||||||
|
expect(provider.authorization.params.scope).toBe("openid email");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
provider.profile({
|
||||||
|
sub: "sub",
|
||||||
|
preferred_username: "user",
|
||||||
|
email: "user@example.com",
|
||||||
|
picture: "https://example.com/p.png",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
id: "sub",
|
||||||
|
name: "user",
|
||||||
|
email: "user@example.com",
|
||||||
|
image: "https://example.com/p.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
provider.profile({
|
||||||
|
id: "id",
|
||||||
|
name: "name",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
id: "id",
|
||||||
|
name: "name",
|
||||||
|
email: null,
|
||||||
|
image: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when only partial OIDC settings are provided", async () => {
|
||||||
|
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
||||||
|
process.env.HOMEPAGE_OIDC_ISSUER = "https://issuer.example";
|
||||||
|
process.env.HOMEPAGE_AUTH_SECRET = "auth-secret";
|
||||||
|
|
||||||
|
await expect(import("pages/api/auth/[...nextauth]")).rejects.toThrow(
|
||||||
|
/OIDC auth is enabled but required settings are missing/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
78
src/__tests__/pages/auth/signin.test.jsx
Normal file
78
src/__tests__/pages/auth/signin.test.jsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const { getSettingsMock } = vi.hoisted(() => ({
|
||||||
|
getSettingsMock: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("utils/config/config", () => ({
|
||||||
|
getSettings: getSettingsMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("next/router", () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
query: {},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { getProviders } from "next-auth/react";
|
||||||
|
import SignInPage, { getServerSideProps } from "pages/auth/signin";
|
||||||
|
|
||||||
|
describe("pages/auth/signin", () => {
|
||||||
|
it("renders an error state when no providers are configured", async () => {
|
||||||
|
render(
|
||||||
|
<SignInPage
|
||||||
|
providers={{}}
|
||||||
|
settings={{
|
||||||
|
theme: "dark",
|
||||||
|
color: "slate",
|
||||||
|
title: "Homepage",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Authentication not configured")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
||||||
|
expect(document.documentElement.classList.contains("scheme-dark")).toBe(true);
|
||||||
|
expect(document.documentElement.classList.contains("theme-slate")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders provider buttons when providers are available", () => {
|
||||||
|
render(
|
||||||
|
<SignInPage
|
||||||
|
providers={{
|
||||||
|
oidc: { id: "oidc", name: "OIDC" },
|
||||||
|
}}
|
||||||
|
settings={{
|
||||||
|
theme: "light",
|
||||||
|
color: "emerald",
|
||||||
|
title: "My Dashboard",
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Sign in")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: /login via oidc/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getServerSideProps returns providers and settings", async () => {
|
||||||
|
getProviders.mockResolvedValueOnce({ foo: { id: "foo", name: "Foo" } });
|
||||||
|
getSettingsMock.mockReturnValueOnce({ theme: "dark" });
|
||||||
|
|
||||||
|
const res = await getServerSideProps({});
|
||||||
|
|
||||||
|
expect(getProviders).toHaveBeenCalled();
|
||||||
|
expect(getSettingsMock).toHaveBeenCalled();
|
||||||
|
expect(res).toEqual({
|
||||||
|
props: {
|
||||||
|
providers: { foo: { id: "foo", name: "Foo" } },
|
||||||
|
settings: { theme: "dark" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,23 +1,34 @@
|
|||||||
|
import { getToken } from "next-auth/jwt";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
export function middleware(req) {
|
const authEnabled = Boolean(process.env.HOMEPAGE_AUTH_ENABLED);
|
||||||
// Check the Host header, if HOMEPAGE_ALLOWED_HOSTS is set
|
const authSecret = process.env.NEXTAUTH_SECRET || process.env.HOMEPAGE_AUTH_SECRET;
|
||||||
const host = req.headers.get("host");
|
let warnedAllowedHosts = false;
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
let allowedHosts = [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`];
|
export async function middleware(req) {
|
||||||
const allowAll = process.env.HOMEPAGE_ALLOWED_HOSTS === "*";
|
if (!warnedAllowedHosts && process.env.HOMEPAGE_ALLOWED_HOSTS) {
|
||||||
if (process.env.HOMEPAGE_ALLOWED_HOSTS) {
|
warnedAllowedHosts = true;
|
||||||
allowedHosts = allowedHosts.concat(process.env.HOMEPAGE_ALLOWED_HOSTS.split(","));
|
console.warn(
|
||||||
}
|
"HOMEPAGE_ALLOWED_HOSTS is deprecated. To secure a publicly accessible homepage, configure authentication instead.",
|
||||||
if (!allowAll && (!host || !allowedHosts.includes(host))) {
|
|
||||||
console.error(
|
|
||||||
`Host validation failed for: ${host}. Hint: Set the HOMEPAGE_ALLOWED_HOSTS environment variable to allow requests from this host / port.`,
|
|
||||||
);
|
);
|
||||||
return NextResponse.json({ error: "Host validation failed. See logs for more details." }, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authEnabled) {
|
||||||
|
const token = await getToken({ req, secret: authSecret });
|
||||||
|
if (!token) {
|
||||||
|
const signInUrl = new URL("/auth/signin", req.url);
|
||||||
|
signInUrl.searchParams.set("callbackUrl", "/");
|
||||||
|
return NextResponse.redirect(signInUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: "/api/:path*",
|
// Protect all app and API routes; allow Next.js internals, public assets, auth pages, and NextAuth endpoints.
|
||||||
|
matcher: [
|
||||||
|
"/",
|
||||||
|
"/((?!_next/static|_next/image|favicon.ico|robots.txt|manifest.json|sitemap.xml|icons/|api/auth|auth/).*)",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,70 +1,89 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const { NextResponse } = vi.hoisted(() => ({
|
const { NextResponse, getToken } = vi.hoisted(() => ({
|
||||||
NextResponse: {
|
NextResponse: {
|
||||||
json: vi.fn((body, init) => ({ type: "json", body, init })),
|
|
||||||
next: vi.fn(() => ({ type: "next" })),
|
next: vi.fn(() => ({ type: "next" })),
|
||||||
|
redirect: vi.fn((url) => ({ type: "redirect", url })),
|
||||||
},
|
},
|
||||||
|
getToken: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("next/server", () => ({ NextResponse }));
|
vi.mock("next/server", () => ({ NextResponse }));
|
||||||
|
vi.mock("next-auth/jwt", () => ({ getToken }));
|
||||||
|
|
||||||
import { middleware } from "./middleware";
|
async function loadMiddleware() {
|
||||||
|
vi.resetModules();
|
||||||
|
const mod = await import("./middleware");
|
||||||
|
return mod.middleware;
|
||||||
|
}
|
||||||
|
|
||||||
function createReq(host) {
|
function createReq(url = "http://localhost:3000/") {
|
||||||
return {
|
return {
|
||||||
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
get: (key) => (key === "host" ? host : null),
|
get: () => null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("middleware", () => {
|
describe("middleware", () => {
|
||||||
const originalEnv = process.env;
|
const originalEnv = process.env;
|
||||||
const originalConsoleError = console.error;
|
const originalConsoleWarn = console.warn;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
process.env = { ...originalEnv };
|
process.env = { ...originalEnv };
|
||||||
console.error = originalConsoleError;
|
console.warn = originalConsoleWarn;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows requests for default localhost hosts", () => {
|
it("allows requests when auth is disabled", async () => {
|
||||||
process.env.PORT = "3000";
|
const middleware = await loadMiddleware();
|
||||||
const res = middleware(createReq("localhost:3000"));
|
const res = await middleware(createReq());
|
||||||
|
|
||||||
expect(NextResponse.next).toHaveBeenCalled();
|
expect(NextResponse.next).toHaveBeenCalled();
|
||||||
expect(res).toEqual({ type: "next" });
|
expect(res).toEqual({ type: "next" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks requests when host is not allowed", () => {
|
it("warns once when HOMEPAGE_ALLOWED_HOSTS is set, but does not block", async () => {
|
||||||
process.env.PORT = "3000";
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
process.env.HOMEPAGE_ALLOWED_HOSTS = "example.com";
|
||||||
|
|
||||||
const res = middleware(createReq("evil.com"));
|
const middleware = await loadMiddleware();
|
||||||
|
const res1 = await middleware(createReq());
|
||||||
expect(errSpy).toHaveBeenCalled();
|
const res2 = await middleware(createReq());
|
||||||
expect(NextResponse.json).toHaveBeenCalledWith(
|
|
||||||
{ error: "Host validation failed. See logs for more details." },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
expect(res.type).toBe("json");
|
|
||||||
expect(res.init.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows requests when HOMEPAGE_ALLOWED_HOSTS is '*'", () => {
|
|
||||||
process.env.HOMEPAGE_ALLOWED_HOSTS = "*";
|
|
||||||
const res = middleware(createReq("anything.example"));
|
|
||||||
|
|
||||||
|
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(NextResponse.next).toHaveBeenCalled();
|
expect(NextResponse.next).toHaveBeenCalled();
|
||||||
expect(res).toEqual({ type: "next" });
|
expect(res1).toEqual({ type: "next" });
|
||||||
|
expect(res2).toEqual({ type: "next" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows requests when host is included in HOMEPAGE_ALLOWED_HOSTS", () => {
|
it("redirects to signin when auth is enabled and no token is present", async () => {
|
||||||
process.env.PORT = "3000";
|
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
||||||
process.env.HOMEPAGE_ALLOWED_HOSTS = "example.com:3000,other:3000";
|
process.env.HOMEPAGE_AUTH_SECRET = "secret";
|
||||||
|
|
||||||
const res = middleware(createReq("example.com:3000"));
|
getToken.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
const middleware = await loadMiddleware();
|
||||||
|
const res = await middleware(createReq("http://localhost:3000/some"));
|
||||||
|
|
||||||
|
expect(getToken).toHaveBeenCalledWith({
|
||||||
|
req: expect.objectContaining({ url: "http://localhost:3000/some" }),
|
||||||
|
secret: "secret",
|
||||||
|
});
|
||||||
|
expect(NextResponse.redirect).toHaveBeenCalled();
|
||||||
|
expect(res.type).toBe("redirect");
|
||||||
|
expect(String(res.url)).toContain("/auth/signin");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows requests when auth is enabled and a token is present", async () => {
|
||||||
|
process.env.HOMEPAGE_AUTH_ENABLED = "true";
|
||||||
|
process.env.HOMEPAGE_AUTH_SECRET = "secret";
|
||||||
|
|
||||||
|
getToken.mockResolvedValueOnce({ sub: "user" });
|
||||||
|
|
||||||
|
const middleware = await loadMiddleware();
|
||||||
|
const res = await middleware(createReq("http://localhost:3000/"));
|
||||||
|
|
||||||
expect(NextResponse.next).toHaveBeenCalled();
|
expect(NextResponse.next).toHaveBeenCalled();
|
||||||
expect(res).toEqual({ type: "next" });
|
expect(res).toEqual({ type: "next" });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import { SessionProvider } from "next-auth/react";
|
||||||
import { appWithTranslation } from "next-i18next";
|
import { appWithTranslation } from "next-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import "styles/globals.css";
|
import "styles/globals.css";
|
||||||
@@ -69,6 +70,7 @@ const tailwindSafelist = [
|
|||||||
|
|
||||||
function MyApp({ Component, pageProps }) {
|
function MyApp({ Component, pageProps }) {
|
||||||
return (
|
return (
|
||||||
|
<SessionProvider session={pageProps.session}>
|
||||||
<SWRConfig
|
<SWRConfig
|
||||||
value={{
|
value={{
|
||||||
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
|
fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()),
|
||||||
@@ -91,6 +93,7 @@ function MyApp({ Component, pageProps }) {
|
|||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ColorProvider>
|
</ColorProvider>
|
||||||
</SWRConfig>
|
</SWRConfig>
|
||||||
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
114
src/pages/api/auth/[...nextauth].js
Normal file
114
src/pages/api/auth/[...nextauth].js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { timingSafeEqual } from "node:crypto";
|
||||||
|
|
||||||
|
import NextAuth from "next-auth";
|
||||||
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
|
|
||||||
|
const authEnabled = Boolean(process.env.HOMEPAGE_AUTH_ENABLED);
|
||||||
|
const issuer = process.env.HOMEPAGE_OIDC_ISSUER;
|
||||||
|
const clientId = process.env.HOMEPAGE_OIDC_CLIENT_ID;
|
||||||
|
const clientSecret = process.env.HOMEPAGE_OIDC_CLIENT_SECRET;
|
||||||
|
const homepageAuthSecret = process.env.HOMEPAGE_AUTH_SECRET;
|
||||||
|
const homepageExternalUrl = process.env.HOMEPAGE_EXTERNAL_URL;
|
||||||
|
const homepageAuthPassword = process.env.HOMEPAGE_AUTH_PASSWORD;
|
||||||
|
|
||||||
|
// Map HOMEPAGE_* envs to what NextAuth expects
|
||||||
|
if (!process.env.NEXTAUTH_SECRET && homepageAuthSecret) {
|
||||||
|
process.env.NEXTAUTH_SECRET = homepageAuthSecret;
|
||||||
|
}
|
||||||
|
if (!process.env.NEXTAUTH_URL && homepageExternalUrl) {
|
||||||
|
process.env.NEXTAUTH_URL = homepageExternalUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultScope = process.env.HOMEPAGE_OIDC_SCOPE || "openid email profile";
|
||||||
|
const cleanedIssuer = issuer ? issuer.replace(/\/+$/, "") : issuer;
|
||||||
|
const hasOidcConfig = Boolean(issuer && clientId && clientSecret);
|
||||||
|
const hasAnyOidcConfig = Boolean(issuer || clientId || clientSecret);
|
||||||
|
|
||||||
|
if (authEnabled) {
|
||||||
|
if (hasOidcConfig) {
|
||||||
|
if (!process.env.NEXTAUTH_SECRET || !process.env.NEXTAUTH_URL) {
|
||||||
|
throw new Error("OIDC auth is enabled but required settings are missing.");
|
||||||
|
}
|
||||||
|
} else if (hasAnyOidcConfig) {
|
||||||
|
throw new Error("OIDC auth is enabled but required settings are missing.");
|
||||||
|
} else if (!homepageAuthPassword || !process.env.NEXTAUTH_SECRET) {
|
||||||
|
throw new Error("Password auth is enabled but required settings are missing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let providers = [];
|
||||||
|
if (authEnabled) {
|
||||||
|
if (hasOidcConfig) {
|
||||||
|
providers = [
|
||||||
|
{
|
||||||
|
id: "homepage-oidc",
|
||||||
|
name: process.env.HOMEPAGE_OIDC_NAME || "Homepage OIDC",
|
||||||
|
type: "oauth",
|
||||||
|
idToken: true,
|
||||||
|
issuer: cleanedIssuer,
|
||||||
|
wellKnown: `${cleanedIssuer}/.well-known/openid-configuration`,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
authorization: {
|
||||||
|
params: {
|
||||||
|
scope: defaultScope,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
profile(profile) {
|
||||||
|
return {
|
||||||
|
id: profile.sub ?? profile.id ?? profile.user_id ?? profile.uid ?? profile.email,
|
||||||
|
name: profile.name ?? profile.preferred_username ?? profile.nickname ?? profile.email,
|
||||||
|
email: profile.email ?? null,
|
||||||
|
image: profile.picture ?? null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
providers = [
|
||||||
|
CredentialsProvider({
|
||||||
|
name: "Password",
|
||||||
|
credentials: {
|
||||||
|
password: { label: "Password", type: "password" },
|
||||||
|
},
|
||||||
|
async authorize(credentials) {
|
||||||
|
const provided = credentials?.password ?? "";
|
||||||
|
const expected = homepageAuthPassword ?? "";
|
||||||
|
if (!expected || provided.length !== expected.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isMatch = timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
|
||||||
|
if (!isMatch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: "homepage",
|
||||||
|
name: "Homepage",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NextAuth({
|
||||||
|
providers,
|
||||||
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
},
|
||||||
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
|
pages: {
|
||||||
|
signIn: "/auth/signin",
|
||||||
|
},
|
||||||
|
debug: true,
|
||||||
|
logger: {
|
||||||
|
error: (...args) => console.error("[nextauth][error]", ...args),
|
||||||
|
warn: (...args) => console.warn("[nextauth][warn]", ...args),
|
||||||
|
debug: (...args) => console.debug("[nextauth][debug]", ...args),
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
signIn: async (message) => console.debug("[nextauth][event][signIn]", message),
|
||||||
|
signOut: async (message) => console.debug("[nextauth][event][signOut]", message),
|
||||||
|
error: async (message) => console.error("[nextauth][event][error]", message),
|
||||||
|
},
|
||||||
|
});
|
||||||
210
src/pages/auth/signin.jsx
Normal file
210
src/pages/auth/signin.jsx
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
|
import { getProviders, signIn } from "next-auth/react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { BiShieldQuarter } from "react-icons/bi";
|
||||||
|
|
||||||
|
import { getSettings } from "utils/config/config";
|
||||||
|
|
||||||
|
export default function SignIn({ providers, settings }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const theme = settings?.theme || "dark";
|
||||||
|
const color = settings?.color || "slate";
|
||||||
|
const title = settings?.title || "Homepage";
|
||||||
|
const callbackUrl = useMemo(() => {
|
||||||
|
const value = router.query?.callbackUrl;
|
||||||
|
return typeof value === "string" ? value : "/";
|
||||||
|
}, [router.query?.callbackUrl]);
|
||||||
|
const error = router.query?.error;
|
||||||
|
|
||||||
|
let backgroundImage = "";
|
||||||
|
let opacity = settings?.backgroundOpacity ?? 0;
|
||||||
|
let backgroundBlur = false;
|
||||||
|
let backgroundSaturate = false;
|
||||||
|
let backgroundBrightness = false;
|
||||||
|
|
||||||
|
if (settings?.background) {
|
||||||
|
const bg = settings.background;
|
||||||
|
if (typeof bg === "object") {
|
||||||
|
backgroundImage = bg.image || "";
|
||||||
|
if (bg.opacity !== undefined) {
|
||||||
|
opacity = 1 - bg.opacity / 100;
|
||||||
|
}
|
||||||
|
backgroundBlur = bg.blur !== undefined;
|
||||||
|
backgroundSaturate = bg.saturate !== undefined;
|
||||||
|
backgroundBrightness = bg.brightness !== undefined;
|
||||||
|
} else {
|
||||||
|
backgroundImage = bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
|
html.classList.remove("dark", "scheme-dark", "scheme-light");
|
||||||
|
html.classList.toggle("dark", theme === "dark");
|
||||||
|
html.classList.add(theme === "dark" ? "scheme-dark" : "scheme-light");
|
||||||
|
|
||||||
|
const desiredThemeClass = `theme-${color}`;
|
||||||
|
const themeClassesToRemove = Array.from(html.classList).filter(
|
||||||
|
(cls) => cls.startsWith("theme-") && cls !== desiredThemeClass,
|
||||||
|
);
|
||||||
|
if (themeClassesToRemove.length) {
|
||||||
|
html.classList.remove(...themeClassesToRemove);
|
||||||
|
}
|
||||||
|
if (!html.classList.contains(desiredThemeClass)) {
|
||||||
|
html.classList.add(desiredThemeClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.style.backgroundImage = "";
|
||||||
|
body.style.backgroundColor = "";
|
||||||
|
body.style.backgroundAttachment = "";
|
||||||
|
}, [color, theme]);
|
||||||
|
|
||||||
|
if (!providers || Object.keys(providers).length === 0) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{backgroundImage && (
|
||||||
|
<div
|
||||||
|
id="background"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${backgroundImage}')`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<main
|
||||||
|
className={classNames(
|
||||||
|
"relative flex min-h-screen items-center justify-center px-6 py-12",
|
||||||
|
backgroundBlur &&
|
||||||
|
`backdrop-blur${settings?.background?.blur?.length ? `-${settings.background.blur}` : ""}`,
|
||||||
|
backgroundSaturate && `backdrop-saturate-${settings.background.saturate}`,
|
||||||
|
backgroundBrightness && `backdrop-brightness-${settings.background.brightness}`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative w-full max-w-xl overflow-hidden rounded-3xl border border-white/40 bg-white/80 p-10 text-center shadow-2xl shadow-black/10 dark:border-white/10 dark:bg-slate-900/70">
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute inset-x-0 top-0 h-24 bg-gradient-to-r from-theme-500/20 via-theme-500/5 to-transparent"
|
||||||
|
/>
|
||||||
|
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-2xl bg-theme-500/15 text-theme-600 dark:text-theme-300">
|
||||||
|
<BiShieldQuarter className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<h1 className="mt-6 text-2xl font-semibold text-gray-900 dark:text-slate-100">
|
||||||
|
Authentication not configured
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 text-sm text-gray-600 dark:text-slate-400">OIDC is disabled or misconfigured.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordProvider = providers
|
||||||
|
? Object.values(providers).find((provider) => provider.type === "credentials")
|
||||||
|
: null;
|
||||||
|
const hasPasswordProvider = Boolean(passwordProvider);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{backgroundImage && (
|
||||||
|
<div
|
||||||
|
id="background"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `linear-gradient(rgb(var(--bg-color) / ${opacity}), rgb(var(--bg-color) / ${opacity})), url('${backgroundImage}')`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<main className="relative flex min-h-screen items-center justify-center px-6 py-12">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"relative w-full max-w-4xl overflow-hidden rounded-3xl border border-white/50 bg-white/80 shadow-2xl shadow-black/10 backdrop-blur-xl dark:border-white/10 dark:bg-slate-950/70",
|
||||||
|
backgroundBlur &&
|
||||||
|
`backdrop-blur${settings?.background?.blur?.length ? `-${settings.background.blur}` : ""}`,
|
||||||
|
backgroundSaturate && `backdrop-saturate-${settings.background.saturate}`,
|
||||||
|
backgroundBrightness && `backdrop-brightness-${settings.background.brightness}`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="pointer-events-none absolute -left-24 -top-20 h-64 w-64 rounded-full bg-theme-500/20 blur-3xl" />
|
||||||
|
<div className="pointer-events-none absolute -bottom-24 right-0 h-72 w-72 rounded-full bg-theme-500/10 blur-3xl" />
|
||||||
|
<div className="grid gap-10 px-8 py-12 md:grid-cols-[1.2fr_1fr] md:px-12">
|
||||||
|
<section className="flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-full border border-theme-500/30 bg-theme-500/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-theme-600 dark:text-theme-300">
|
||||||
|
Login Required
|
||||||
|
</div>
|
||||||
|
<h1 className="mt-6 text-3xl font-semibold text-gray-900 dark:text-slate-100">{title}</h1>
|
||||||
|
<p className="mt-3 text-sm text-gray-600 dark:text-slate-300">Login to view your dashboard.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex flex-col justify-center gap-6">
|
||||||
|
<div className="rounded-2xl border border-white/60 bg-white/70 p-6 shadow-lg shadow-black/5 dark:border-white/10 dark:bg-slate-900/70">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-slate-100">Sign in</h2>
|
||||||
|
<div className="mt-6 space-y-3">
|
||||||
|
{hasPasswordProvider && (
|
||||||
|
<form
|
||||||
|
className="space-y-3"
|
||||||
|
onSubmit={async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await signIn(passwordProvider?.id ?? "credentials", {
|
||||||
|
redirect: true,
|
||||||
|
callbackUrl,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-slate-300">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
|
autoComplete="current-password"
|
||||||
|
className="w-full rounded-xl border border-slate-200 bg-white/90 px-4 py-3 text-sm text-gray-900 shadow-sm outline-none ring-0 transition focus:border-theme-500 focus:ring-2 focus:ring-theme-500/30 dark:border-slate-700 dark:bg-slate-900/60 dark:text-slate-100"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="group w-full rounded-xl bg-theme-600 px-4 py-3 text-sm font-semibold text-white shadow-lg shadow-theme-600/20 transition hover:-translate-y-0.5 hover:bg-theme-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-theme-500"
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">Sign in →</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
{!hasPasswordProvider &&
|
||||||
|
Object.values(providers).map((provider) => (
|
||||||
|
<button
|
||||||
|
key={provider.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => signIn(provider.id, { callbackUrl })}
|
||||||
|
className="group w-full rounded-xl bg-theme-600 px-4 py-3 text-sm font-semibold text-white shadow-lg shadow-theme-600/20 transition hover:-translate-y-0.5 hover:bg-theme-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-theme-500"
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center gap-2">Login via {provider.name} →</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{hasPasswordProvider && error && (
|
||||||
|
<p className="mt-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-800/60 dark:bg-red-950/40 dark:text-red-200">
|
||||||
|
Invalid password. Please try again.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServerSideProps(context) {
|
||||||
|
const providers = await getProviders();
|
||||||
|
const settings = getSettings();
|
||||||
|
return {
|
||||||
|
props: { providers, settings },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ export function formatApiCall(url, args) {
|
|||||||
if (key === "url") {
|
if (key === "url") {
|
||||||
value = value.replace(/\/+$/, ""); // remove trailing slashes
|
value = value.replace(/\/+$/, ""); // remove trailing slashes
|
||||||
}
|
}
|
||||||
return value?.toString() || "";
|
return value || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
return url.replace(find, replace).replace(find, replace);
|
return url.replace(find, replace).replace(find, replace);
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ export default function Component({ service }) {
|
|||||||
const { data: images, error: imagesError } = useWidgetAPI(widget, "images");
|
const { data: images, error: imagesError } = useWidgetAPI(widget, "images");
|
||||||
const { data: updates, error: updatesError } = useWidgetAPI(widget, "updates");
|
const { data: updates, error: updatesError } = useWidgetAPI(widget, "updates");
|
||||||
|
|
||||||
const error =
|
const error = containersError ?? imagesError ?? updatesError;
|
||||||
containersError ?? imagesError ?? updatesError ?? containers?.detail ?? images?.detail ?? updates?.detail;
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <Container service={service} error={error} />;
|
return <Container service={service} error={error} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,16 +34,6 @@ describe("widgets/arcane/component", () => {
|
|||||||
expect(screen.getByText("arcane.environment_required")).toBeInTheDocument();
|
expect(screen.getByText("arcane.environment_required")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows an error when API calls return detail errors", () => {
|
|
||||||
useWidgetAPI.mockImplementation(() => ({ data: { detail: "Specific API error" }, error: undefined }));
|
|
||||||
|
|
||||||
renderWithProviders(<Component service={{ widget: { type: "arcane", env: "prod" } }} />, {
|
|
||||||
settings: { hideErrors: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText("Specific API error")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders placeholders while loading data", () => {
|
it("renders placeholders while loading data", () => {
|
||||||
useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
|
useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
|
||||||
|
|
||||||
|
|||||||
139
uv.lock
generated
139
uv.lock
generated
@@ -1,139 +0,0 @@
|
|||||||
version = 1
|
|
||||||
revision = 1
|
|
||||||
requires-python = ">=3.13"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "click"
|
|
||||||
version = "8.3.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorama"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "deepmerge"
|
|
||||||
version = "2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "homepage-docs"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = { virtual = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "zensical" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [{ name = "zensical", specifier = ">=0.0.21" }]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "markdown"
|
|
||||||
version = "3.10.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pygments"
|
|
||||||
version = "2.19.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pymdown-extensions"
|
|
||||||
version = "10.20.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "markdown" },
|
|
||||||
{ name = "pyyaml" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/6c/9e370934bfa30e889d12e61d0dae009991294f40055c238980066a7fbd83/pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a", size = 852860 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", size = 268768 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyyaml"
|
|
||||||
version = "6.0.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zensical"
|
|
||||||
version = "0.0.21"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "click" },
|
|
||||||
{ name = "deepmerge" },
|
|
||||||
{ name = "markdown" },
|
|
||||||
{ name = "pygments" },
|
|
||||||
{ name = "pymdown-extensions" },
|
|
||||||
{ name = "pyyaml" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/50/2655b5f72d0c72f4366be580f5e2354ff05280d047ea986fe89570e44589/zensical-0.0.21.tar.gz", hash = "sha256:c13563836fa63a3cabeffd83fe3a770ca740cfa5ae7b85df85d89837e31b3b4a", size = 3819731 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/98/90710d232cb35b633815fa7b493da542391b89283b6103a5bb4ae9fc0dd9/zensical-0.0.21-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:67404cc70c330246dfb7269bcdb60a25be0bb60a212a09c9c50229a1341b1f84", size = 12237120 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/fb/4280b3781157e8f051711732192f949bf29beeafd0df3e33c1c8bf9b7a1a/zensical-0.0.21-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4fd253ccfbf5af56434124f13bac01344e456c020148369b18d8836b6537c3c", size = 12118047 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/b3/b7f85ae9cf920cf9f17bf157ae6c274919477148feb7716bf735636caa0e/zensical-0.0.21-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:440e40cdc30a29bf7466bcd6f43ed7bd1c54ea3f1a0fefca65619358b481a5bc", size = 12473440 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/ac/1dc6e98f79ed19b9f103c88a0bd271f9140565d7d26b64bc1542b3ef6d91/zensical-0.0.21-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:368e832fc8068e75dc45cab59379db4cefcd81eb116f48d058db8fb7b7aa8d14", size = 12412588 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/76/16a580f6dd32b387caa4a41615451e7dddd1917a2ff2e5b08744f41b4e11/zensical-0.0.21-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4ab962d47f9dd73510eed168469326c7a452554dfbfdb9cdf85efc7140244df", size = 12749438 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/30/4baaa1c910eee61db5f49d0d45f2e550a0027218c618f3dd7f8da966a019/zensical-0.0.21-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b846d53dfce007f056ff31848f87f3f2a388228e24d4851c0cafdce0fa204c9b", size = 12514504 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/77/931fccae5580b94409a0448a26106f922dcfa7822e7b93cacd2876dd63a8/zensical-0.0.21-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:daac1075552d230d52d621d2e4754ba24d5afcaa201a7a991f1a8d57e320c9de", size = 12647832 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/82/3cf75de64340829d55c87c36704f4d1d8c952bd2cdc8a7bc48cbfb8ab333/zensical-0.0.21-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:7b380f545adb6d40896f9bd698eb0e1540ed4258d35b83f55f91658d0fdae312", size = 12678537 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/91/6f4938dceeaa241f78bbfaf58a94acef10ba18be3468795173e3087abeb6/zensical-0.0.21-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c2227fdab64616bea94b40b8340bafe00e2e23631cc58eeea1e7267167e6ac5", size = 12822164 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/4e/a9c9d25ef0766f767db7b4f09da68da9b3d8a28c3d68cfae01f8e3f9e297/zensical-0.0.21-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2e0f5154d236ed0f98662ee68785b67e8cd2138ea9d5e26070649e93c22eeee0", size = 12785632 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/76/8627548f6d00e676009a129be49bb22ca2aa3c938df9b642eea18c381c98/zensical-0.0.21-cp310-abi3-win32.whl", hash = "sha256:0b72e1f8a880c130cbd52335e3bc5dff666291cec40eaf09061d5d7a5a9f8068", size = 11832998 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/9a/7767cfcf70a2022b2b4d49c48f022fde3feb3d3360ac084c111e75df96cf/zensical-0.0.21-cp310-abi3-win_amd64.whl", hash = "sha256:eec08ff6fa335c11b81599032aaeda91c65d31ef03b97aa9239074bf19963a98", size = 12033476 },
|
|
||||||
]
|
|
||||||
@@ -8,6 +8,12 @@ afterEach(() => {
|
|||||||
if (typeof document !== "undefined") cleanup();
|
if (typeof document !== "undefined") cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Avoid NextAuth client-side fetches during unit tests.
|
||||||
|
vi.mock("next-auth/react", () => ({
|
||||||
|
SessionProvider: ({ children }) => children ?? null,
|
||||||
|
getProviders: vi.fn(async () => ({})),
|
||||||
|
}));
|
||||||
|
|
||||||
// implement a couple of common formatters mocked in next-i18next
|
// implement a couple of common formatters mocked in next-i18next
|
||||||
vi.mock("next-i18next", () => ({
|
vi.mock("next-i18next", () => ({
|
||||||
// Keep app/page components importable in unit tests.
|
// Keep app/page components importable in unit tests.
|
||||||
|
|||||||
Reference in New Issue
Block a user