Compare commits
133
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
024ec9235d | ||
|
|
58fdea0245 | ||
|
|
4ead238462 | ||
|
|
6305c74f90 | ||
|
|
0780080681 | ||
|
|
aa15a705d7 | ||
|
|
cc70175125 | ||
|
|
bc5e25e9cd | ||
|
|
d43557e165 | ||
|
|
b6c96f73a5 | ||
|
|
ee8831affe | ||
|
|
6e21863d04 | ||
|
|
ca39ca66f0 | ||
|
|
cc7d53e84e | ||
|
|
99c409cbb7 | ||
|
|
1fcf9d0146 | ||
|
|
a234fc844e | ||
|
|
168b587c18 | ||
|
|
dc12a82805 | ||
|
|
f5076454cd | ||
|
|
aeb681a858 | ||
|
|
56b57ad171 | ||
|
|
91e1cef611 | ||
|
|
e53b06e8c2 | ||
|
|
0ebf4bc55e | ||
|
|
b8a155dd08 | ||
|
|
63786eed06 | ||
|
|
598f0047eb | ||
|
|
eacda4c397 | ||
|
|
5cb726f793 | ||
|
|
e8d29420e0 | ||
|
|
23c7d0bbf0 | ||
|
|
75aefcb0d5 | ||
|
|
b52a8a7935 | ||
|
|
6d90209e00 | ||
|
|
8f0945f51a | ||
|
|
5b04b93f23 | ||
|
|
c8228b920c | ||
|
|
abf7942fb7 | ||
|
|
50746eb3e4 | ||
|
|
76abfca11b | ||
|
|
fcab6a347d | ||
|
|
c91aae53ac | ||
|
|
4943e7b851 | ||
|
|
88c423fca6 | ||
|
|
f238663ec7 | ||
|
|
e9870e7ee7 | ||
|
|
9c0a0ced04 | ||
|
|
4c27635066 | ||
|
|
3d66ff9148 | ||
|
|
ec257e5f60 | ||
|
|
ef24370a8c | ||
|
|
1fa0fc50c5 | ||
|
|
e9a1dae857 | ||
|
|
e37279fe80 | ||
|
|
82c6ce7b77 | ||
|
|
555212e2dc | ||
|
|
c831539772 | ||
|
|
3e2ffbd7bf | ||
|
|
22d7f676eb | ||
|
|
23bf262f4e | ||
|
|
ecae80d741 | ||
|
|
162e8adf4c | ||
|
|
620ec0315f | ||
|
|
8df398c3c7 | ||
|
|
425359be02 | ||
|
|
8893b72fe3 | ||
|
|
b9483379dd | ||
|
|
0336803550 | ||
|
|
e220087e96 | ||
|
|
f127c9325c | ||
|
|
c410b06a15 | ||
|
|
ce755efe4d | ||
|
|
8b416a6dd3 | ||
|
|
fe95334aa7 | ||
|
|
b0facfd77a | ||
|
|
01db27c34f | ||
|
|
999cf58625 | ||
|
|
9223dd06ee | ||
|
|
42da2b9b06 | ||
|
|
a73eed63eb | ||
|
|
44dcc72d43 | ||
|
|
d02ded5511 | ||
|
|
dfacb08dbd | ||
|
|
4e85bbbad0 | ||
|
|
d78c6ed529 | ||
|
|
210ce3669f | ||
|
|
44e5346d75 | ||
|
|
d34d665229 | ||
|
|
b6dc38483d | ||
|
|
a7e387e54c | ||
|
|
189f35f3cb | ||
|
|
3c8d4f5018 | ||
|
|
2b59f19a7f | ||
|
|
fff1da88bd | ||
|
|
720fdb772c | ||
|
|
a225bdd70c | ||
|
|
e40c348352 | ||
|
|
28e1175565 | ||
|
|
b450b00934 | ||
|
|
8e0bf87c56 | ||
|
|
9e061f8fc7 | ||
|
|
10855bd84c | ||
|
|
93d5ca683b | ||
|
|
d2584b63c5 | ||
|
|
8c1156240c | ||
|
|
bb7f751481 | ||
|
|
1e8a5359b7 | ||
|
|
550bca6dee | ||
|
|
2ae3d938ee | ||
|
|
2b4bc8352e | ||
|
|
c0f681637d | ||
|
|
92cd1c5bd0 | ||
|
|
3b7e6cc083 | ||
|
|
61afab518f | ||
|
|
a7bd664c5c | ||
|
|
a2c3bee66c | ||
|
|
b6dad438c2 | ||
|
|
044c60bdec | ||
|
|
de8485d5c3 | ||
|
|
26b6ca2445 | ||
|
|
63a7be35a4 | ||
|
|
1baa9fc74c | ||
|
|
a51999457b | ||
|
|
5fbbf037ce | ||
|
|
3c0f272707 | ||
|
|
d2b4bf5ae4 | ||
|
|
d803087345 | ||
|
|
d3a883a252 | ||
|
|
694350583a | ||
|
|
ecc4586d02 | ||
|
|
ce4fa5ff67 | ||
|
|
649506b56a |
No files matched your search
+2
-1
@@ -6,4 +6,5 @@ README.md
|
||||
.next
|
||||
.git
|
||||
dev
|
||||
.build
|
||||
.build
|
||||
e2e
|
||||
+8
-5
@@ -4,6 +4,14 @@
|
||||
# This file will be committed to version control, so make sure not to have any secrets in it.
|
||||
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
|
||||
|
||||
# The below secret is not used anywhere but required for Auth.js (Would encrypt JWTs and Mail hashes, both not used)
|
||||
AUTH_SECRET="supersecret"
|
||||
|
||||
# The below secret is used to encrypt integration secrets in the database.
|
||||
# It should be a 32-byte string, generated by running `openssl rand -hex 32` on Unix
|
||||
# or starting the project without any (which will show a randomly generated one).
|
||||
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# This is how you can use the sqlite driver:
|
||||
DB_DRIVER='better-sqlite3'
|
||||
DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
|
||||
@@ -20,11 +28,6 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
|
||||
# DB_PASSWORD='password'
|
||||
# DB_NAME='name-of-database'
|
||||
|
||||
|
||||
# You can generate the secret via 'openssl rand -base64 32' on Unix
|
||||
# @see https://next-auth.js.org/configuration/options#secret
|
||||
AUTH_SECRET='supersecret'
|
||||
|
||||
TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
# Configure logging to use winston logger
|
||||
|
||||
@@ -1,37 +1,78 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Create a bug report to help us improve
|
||||
description: Report that something is broken, not working as intended or causes side-effects
|
||||
title: "bug: "
|
||||
labels: ["🐞❔ unconfirmed bug"]
|
||||
labels: ["needs triage"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Provide environment information
|
||||
description: |
|
||||
Run this command in your project root and paste the results in a code block:
|
||||
```bash
|
||||
npx envinfo --system --binaries
|
||||
```
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Link to reproduction
|
||||
description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored.
|
||||
label: Steps to reproduce
|
||||
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To reproduce
|
||||
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
|
||||
label: Impact
|
||||
description: How big is the impact of this bug? Does it make Homarr unusable? Is there any workaround that you're aware of?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the bug here, screenshots if applicable.
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of Homarr are you running?
|
||||
options:
|
||||
- 1.0.0-beta
|
||||
- Other (describe in "additional information")
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: installationMethod
|
||||
attributes:
|
||||
label: Installation method
|
||||
description: How do you run Homarr? Post docker-compose, configs or screenshots if applicable.
|
||||
options:
|
||||
- Docker Run
|
||||
- Docker Compose
|
||||
- Portainer
|
||||
- Helm
|
||||
- QNAP
|
||||
- Saltbox
|
||||
- EasyPanel
|
||||
- Unraid Apps
|
||||
- TrueNAS Apps
|
||||
- Synology
|
||||
- HomeAssistant Addon
|
||||
- From Source
|
||||
- Other (describe in "additional information")
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: If relevant, what browser do you use?
|
||||
options:
|
||||
- Firefox
|
||||
- Edge (Chromium)
|
||||
- Edge (Proprietary)
|
||||
- Chrome
|
||||
- Safari
|
||||
- Vivaldi
|
||||
- Brave
|
||||
- Samsung Internet
|
||||
- Other (describe in "additional information")
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
@@ -2,26 +2,16 @@
|
||||
# See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml
|
||||
|
||||
name: 🛠 Feature Request
|
||||
description: Create a feature request for the core packages
|
||||
description: Request a new feature that you would like to have implemented
|
||||
title: "feat: "
|
||||
labels: ["✨ enhancement"]
|
||||
labels: ["needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to file a feature request. Please fill out this form as completely as possible.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you'd like to request
|
||||
description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like to see
|
||||
description: Please describe the solution you would like to see. Adding example usage is a good way to provide context.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
name: 🛠 Integration request
|
||||
description: Request support for a new integration (eg. Sonarr, Radarr)
|
||||
title: "feat: "
|
||||
labels: ["needs triage"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Project Website
|
||||
description: Post the link to the website of the application. Paste the official link.
|
||||
placeholder: ex. https://sonarr.tv/
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe what data should be consumed by Homarr
|
||||
description: Please describe what data Homarr should fetch from the integration. Specify in what interval data should be fetched and whether the user can also perform write operations (eg. deleting a movie or adding a user).
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the integration.
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Public API available?
|
||||
description: Is there a public API available, that we can consume in Homarr?
|
||||
options:
|
||||
- Yes, available on a website
|
||||
- Yes, available in the application itself
|
||||
- No
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Are you willing to contribute this yourself?
|
||||
options:
|
||||
- Yes
|
||||
- No
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,30 @@
|
||||
name: 🛠 Widget request
|
||||
description: Request a new widget (eg. Clock, Calendar, ...)
|
||||
title: "feat: "
|
||||
labels: ["needs triage"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Compatible integrations
|
||||
description: Post a list of the integrations that should be compatible with this widget. Divide using comma. Leave empty if no integration is needed.
|
||||
placeholder: ex. Sonarr, Radarr, Lidarr, Readarr, Nextcloud
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe what data should be displayed
|
||||
description: Please describe what data Homarr should display. Describe how elements should be intractable and what actions the user can perform.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the widget.
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Are you willing to contribute this yourself?
|
||||
options:
|
||||
- Yes
|
||||
- No
|
||||
validations:
|
||||
required: true
|
||||
@@ -91,6 +91,8 @@ jobs:
|
||||
network: host
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: true
|
||||
- name: Install playwright browsers
|
||||
run: pnpm exec playwright install chromium
|
||||
- name: Run E2E Tests
|
||||
shell: bash
|
||||
run: pnpm test:e2e
|
||||
|
||||
@@ -13,11 +13,6 @@ on:
|
||||
required: false
|
||||
default: true
|
||||
description: Send notifications
|
||||
push-image:
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
description: Push Docker Image
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -40,6 +35,7 @@ jobs:
|
||||
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' }}
|
||||
outputs:
|
||||
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
|
||||
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
|
||||
|
||||
steps:
|
||||
- run: echo "Skipping release for workflow_dispatch event"
|
||||
@@ -87,9 +83,16 @@ jobs:
|
||||
run: |
|
||||
git fetch --tags
|
||||
echo "version=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT"
|
||||
- name: Read git ref
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
id: read-git-ref
|
||||
run: |
|
||||
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
- name: Update dev branch
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
continue-on-error: true # Prevent pipeline from failing when merge fails
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
run: |
|
||||
git fetch origin dev
|
||||
git checkout dev
|
||||
@@ -104,9 +107,10 @@ jobs:
|
||||
NEXT_VERSION: ${{ needs.release.outputs.version }}
|
||||
DEPLOY_LATEST: ${{ github.ref_name == 'main' }}
|
||||
DEPLOY_BETA: ${{ github.ref_name == 'beta' }}
|
||||
PUSH_IMAGE: ${{ github.event_name != 'workflow_dispatch' || github.events.inputs.push-image == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.git_ref }}
|
||||
- name: Discord notification
|
||||
if: ${{ github.events.inputs.send-notifications != false }}
|
||||
env:
|
||||
@@ -130,16 +134,16 @@ jobs:
|
||||
with:
|
||||
images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
|
||||
tags: |
|
||||
${{ env.DEPLOY_LATEST == true && 'type=raw,value=latest' || null }}
|
||||
${{ env.DEPLOY_BETA == true && 'type=raw,value=beta' || null }}
|
||||
${{ env.DEPLOY_LATEST == 'true' && 'type=raw,value=latest' || null }}
|
||||
${{ env.DEPLOY_BETA == 'true' && 'type=raw,value=beta' || null }}
|
||||
type=raw,value=${{ env.NEXT_VERSION }}
|
||||
- name: Build and maybe push
|
||||
- name: Build and push
|
||||
id: buildPushAction
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
push: ${{ env.PUSH_IMAGE}}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
network: host
|
||||
@@ -150,4 +154,4 @@ jobs:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@master
|
||||
with:
|
||||
args: "Deployment of image has completed for branch ${{ github.ref_name }}. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'. ${{ env.PUSH_IMAGE == true && '' || 'This was a dry run' }}"
|
||||
args: "Deployment of image has completed for branch ${{ github.ref_name }}. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'."
|
||||
@@ -58,6 +58,8 @@ apps/websocket/wssServer.cjs
|
||||
apps/nextjs/.million/
|
||||
packages/cli/cli.cjs
|
||||
|
||||
# e2e mounts
|
||||
e2e/shared/tmp
|
||||
|
||||
#personal backgrounds
|
||||
apps/nextjs/public/images/background.png
|
||||
Vendored
+1
-1
@@ -39,5 +39,5 @@
|
||||
"i18n-ally.localesPaths": [
|
||||
"packages/translation/src/lang",
|
||||
],
|
||||
"i18n-ally.keystyle": "auto",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
}
|
||||
+120
@@ -1,3 +1,123 @@
|
||||
## [1.0.0-beta.11](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2025-01-03)
|
||||
|
||||
### Features
|
||||
|
||||
* remove import beta badge ([#1829](https://github.com/homarr-labs/homarr/issues/1829)) ([6305c74](https://github.com/homarr-labs/homarr/commit/6305c74f90d943722c915ffbf54b5fd7e68a7dca))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency zod-form-data to ^2.0.5 ([#1822](https://github.com/homarr-labs/homarr/issues/1822)) ([a234fc8](https://github.com/homarr-labs/homarr/commit/a234fc844e5edcef9935efb0725b6ccceadfe69d))
|
||||
* **deps:** update tanstack-query monorepo to ^5.62.12 ([#1843](https://github.com/homarr-labs/homarr/issues/1843)) ([bc5e25e](https://github.com/homarr-labs/homarr/commit/bc5e25e9cd3269a3d8cc8ef521ae75aff50088be))
|
||||
* **icons:** picker does not allow multiple words ([#1827](https://github.com/homarr-labs/homarr/issues/1827)) ([6e21863](https://github.com/homarr-labs/homarr/commit/6e21863d04f7d0598e7e7ca3fb707f9beb09d7ef))
|
||||
* login page in light mode is always black ([#1821](https://github.com/homarr-labs/homarr/issues/1821)) ([cc7d53e](https://github.com/homarr-labs/homarr/commit/cc7d53e84ec8c2dd3698a178ca6f35099194b5d7))
|
||||
* prowlarr integration indexers status changed from id to indexerId ([#1837](https://github.com/homarr-labs/homarr/issues/1837)) ([ee8831a](https://github.com/homarr-labs/homarr/commit/ee8831affe34a33b80a0993aa538c2b16c035f11))
|
||||
|
||||
## [1.0.0-beta.10](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2024-12-31)
|
||||
|
||||
### Features
|
||||
|
||||
* add request media ([#1811](https://github.com/homarr-labs/homarr/issues/1811)) ([91e1cef](https://github.com/homarr-labs/homarr/commit/91e1cef611a47d57b42c91c0c6d49fcf2e6a10bb))
|
||||
* **docker:** add support for pgid and puid env variables ([#1759](https://github.com/homarr-labs/homarr/issues/1759)) ([aeb681a](https://github.com/homarr-labs/homarr/commit/aeb681a85839e31284e8f1566adc4191a68ea385))
|
||||
* **integration:** add search engine creation ([#1816](https://github.com/homarr-labs/homarr/issues/1816)) ([f507645](https://github.com/homarr-labs/homarr/commit/f5076454cd644a99439b697f24bb0b1f871b2205))
|
||||
* **widget:** add minecraft server status widget ([#1801](https://github.com/homarr-labs/homarr/issues/1801)) ([0ebf4bc](https://github.com/homarr-labs/homarr/commit/0ebf4bc55e62caf5917340496abb350d564df255))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency octokit to ^4.0.3 ([#1806](https://github.com/homarr-labs/homarr/issues/1806)) ([75aefcb](https://github.com/homarr-labs/homarr/commit/75aefcb0d5210a53e8a00b675dbbce847aa8add4))
|
||||
* **deps:** update dependency trpc-to-openapi to ^2.1.1 ([#1788](https://github.com/homarr-labs/homarr/issues/1788)) ([4943e7b](https://github.com/homarr-labs/homarr/commit/4943e7b851cd29291b5e8f2f9d0e084b4deeffc0))
|
||||
* **deps:** update dependency typescript-eslint to ^8.19.0 ([#1814](https://github.com/homarr-labs/homarr/issues/1814)) ([598f004](https://github.com/homarr-labs/homarr/commit/598f0047eb13d6d0d6b68c9032a7a6648f4b6aa3))
|
||||
* **deps:** update tiptap monorepo to v2.11.0 ([#1813](https://github.com/homarr-labs/homarr/issues/1813)) ([eacda4c](https://github.com/homarr-labs/homarr/commit/eacda4c397cb384a35e9eec0305f9b26f2a22a4a))
|
||||
* **docker:** replace anonymous docker volume with env variable for encrypting secrets ([#1809](https://github.com/homarr-labs/homarr/issues/1809)) ([56b57ad](https://github.com/homarr-labs/homarr/commit/56b57ad171da170c2d8edd6112092cef0b586852))
|
||||
* **health-monitoring:** add months to uptime, fix status update ([#1792](https://github.com/homarr-labs/homarr/issues/1792)) ([c91aae5](https://github.com/homarr-labs/homarr/commit/c91aae53acf27eab96a9150281f98a2156e9e2af))
|
||||
* **media-server:** redesign widget ([#1775](https://github.com/homarr-labs/homarr/issues/1775)) ([23c7d0b](https://github.com/homarr-labs/homarr/commit/23c7d0bbf01215b680f346e7eef8072d8b9db09e))
|
||||
|
||||
## [1.0.0-beta.9](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2024-12-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update nextjs monorepo to ^14.2.22 ([#1787](https://github.com/homarr-labs/homarr/issues/1787)) ([3d66ff9](https://github.com/homarr-labs/homarr/commit/3d66ff914883a665c0329c8fc4fd087f4d220692))
|
||||
* **deps:** update tanstack-query monorepo to ^5.62.11 ([#1791](https://github.com/homarr-labs/homarr/issues/1791)) ([e9870e7](https://github.com/homarr-labs/homarr/commit/e9870e7ee78227d3be773d9d93aa732760cc8e21))
|
||||
|
||||
## [1.0.0-beta.8](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2024-12-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @ctrl/deluge to ^7.1.0 ([#1776](https://github.com/homarr-labs/homarr/issues/1776)) ([ecae80d](https://github.com/homarr-labs/homarr/commit/ecae80d741213f12ff08f87992cc4d906cbe0085))
|
||||
* **deps:** update dependency @ctrl/transmission to ^7.2.0 ([#1777](https://github.com/homarr-labs/homarr/issues/1777)) ([3e2ffbd](https://github.com/homarr-labs/homarr/commit/3e2ffbd7bf2793ff58136ed75b38e0d96b4a5ca8))
|
||||
* **deps:** update dependency drizzle-orm to ^0.38.3 ([#1783](https://github.com/homarr-labs/homarr/issues/1783)) ([82c6ce7](https://github.com/homarr-labs/homarr/commit/82c6ce7b77023881bd4c74fb43437e777f9b219e))
|
||||
* **deps:** update dependency drizzle-zod to ^0.6.1 ([#1782](https://github.com/homarr-labs/homarr/issues/1782)) ([555212e](https://github.com/homarr-labs/homarr/commit/555212e2dc82c4b71b8c25a5f20964b3441b5ab9))
|
||||
* oldmarr import user casing wrong ([#1784](https://github.com/homarr-labs/homarr/issues/1784)) ([e37279f](https://github.com/homarr-labs/homarr/commit/e37279fe8071773f1a9773e3233bc81cbb336798))
|
||||
|
||||
## [1.0.0-beta.7](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2024-12-24)
|
||||
|
||||
### Features
|
||||
|
||||
* **apps:** remove url variables ([#1771](https://github.com/homarr-labs/homarr/issues/1771)) ([8df398c](https://github.com/homarr-labs/homarr/commit/8df398c3c706369f6990719603a357c161606f81))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** oidc redirect does not respect https protocol ([#1763](https://github.com/homarr-labs/homarr/issues/1763)) ([0336803](https://github.com/homarr-labs/homarr/commit/033680355044b1ebfa5d8fe07b59929bbcfcb3a0))
|
||||
* **board:** item background and border overridden by mantine card styles ([#1769](https://github.com/homarr-labs/homarr/issues/1769)) ([425359b](https://github.com/homarr-labs/homarr/commit/425359be02f66242f7db0dc77bed0355495e21d5))
|
||||
* **board:** tiles order not respected on firefox ([#1770](https://github.com/homarr-labs/homarr/issues/1770)) ([8893b72](https://github.com/homarr-labs/homarr/commit/8893b72fe363691b91b6aca75d4ad37f4b8e9f6a))
|
||||
* copy media link ([#1750](https://github.com/homarr-labs/homarr/issues/1750)) ([d78c6ed](https://github.com/homarr-labs/homarr/commit/d78c6ed5299e1f975c2e85ec3ecad38e00457677))
|
||||
* **deps:** update dependency @ctrl/qbittorrent to ^9.2.0 ([#1768](https://github.com/homarr-labs/homarr/issues/1768)) ([8b416a6](https://github.com/homarr-labs/homarr/commit/8b416a6dd354b22a3e4f527b793dde2c538b1e6a))
|
||||
* **deps:** update dependency eslint-plugin-react to ^7.37.3 ([#1767](https://github.com/homarr-labs/homarr/issues/1767)) ([fe95334](https://github.com/homarr-labs/homarr/commit/fe95334aa70c95faefcf904591fa9d33159b0905))
|
||||
* **deps:** update dependency jotai to ^2.11.0 ([#1756](https://github.com/homarr-labs/homarr/issues/1756)) ([4e85bbb](https://github.com/homarr-labs/homarr/commit/4e85bbbad034f08ab84e331d376b7a9c6f54e507))
|
||||
* **deps:** update dependency mysql2 to v3.12.0 ([#1758](https://github.com/homarr-labs/homarr/issues/1758)) ([d02ded5](https://github.com/homarr-labs/homarr/commit/d02ded5511aa2d54307db0ac7f4e0a5dee5a707d))
|
||||
* **deps:** update dependency react-error-boundary to v5 ([#1748](https://github.com/homarr-labs/homarr/issues/1748)) ([01db27c](https://github.com/homarr-labs/homarr/commit/01db27c34f3daf6e67e35bbd357df31dcb154b41))
|
||||
* **deps:** update dependency typescript-eslint to ^8.18.2 ([#1762](https://github.com/homarr-labs/homarr/issues/1762)) ([9223dd0](https://github.com/homarr-labs/homarr/commit/9223dd06ee0c2b51e036f607d7b3252c61ab77ea))
|
||||
* **deps:** update mantine monorepo to ^7.15.2 ([#1757](https://github.com/homarr-labs/homarr/issues/1757)) ([dfacb08](https://github.com/homarr-labs/homarr/commit/dfacb08dbddb6864646ece2fca6978207f0c58ac))
|
||||
* **deps:** update tanstack-query monorepo to ^5.62.10 ([#1772](https://github.com/homarr-labs/homarr/issues/1772)) ([c410b06](https://github.com/homarr-labs/homarr/commit/c410b06a15ed5426931ad764cc861f66f11a40da))
|
||||
* **deps:** update tanstack-query monorepo to ^5.62.9 ([#1761](https://github.com/homarr-labs/homarr/issues/1761)) ([42da2b9](https://github.com/homarr-labs/homarr/commit/42da2b9b062bd8a08c37e45ab129953e8b0e3624))
|
||||
* **icons:** local icon repository does not work with medias ([#1765](https://github.com/homarr-labs/homarr/issues/1765)) ([e220087](https://github.com/homarr-labs/homarr/commit/e220087e96c7096303b1d245ccf3941118e692f3))
|
||||
* **onboard:** missing admin permission assignment for external group ([#1764](https://github.com/homarr-labs/homarr/issues/1764)) ([b948337](https://github.com/homarr-labs/homarr/commit/b9483379dd56a8dab2b9b636b66b970da4cf9d83))
|
||||
|
||||
## [1.0.0-beta.6](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2024-12-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency ioredis to v5.4.2 ([#1739](https://github.com/homarr-labs/homarr/issues/1739)) ([720fdb7](https://github.com/homarr-labs/homarr/commit/720fdb772c2142e23b2bfa2ecaddf0063fbe192c))
|
||||
* **deps:** update dependency next-intl to v3.26.3 ([#1740](https://github.com/homarr-labs/homarr/issues/1740)) ([fff1da8](https://github.com/homarr-labs/homarr/commit/fff1da88bd44162b08a76f8b2f49e2b929fc56e8))
|
||||
|
||||
## [1.0.0-beta.5](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2024-12-19)
|
||||
|
||||
### Features
|
||||
|
||||
* Add quick filters for integration and status + minor UI improvement from feedback ([#1641](https://github.com/homarr-labs/homarr/issues/1641)) ([b6dad43](https://github.com/homarr-labs/homarr/commit/b6dad438c2f44aa8d7a740c1a22c9d4c39edf43e))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* about version is one behind because of missing package-json update in deploy ([#1725](https://github.com/homarr-labs/homarr/issues/1725)) ([8c11562](https://github.com/homarr-labs/homarr/commit/8c1156240c8a0d332301ae6dd0a4521f3db19d9a))
|
||||
* allow additional file-type for oldmarr import ([#1723](https://github.com/homarr-labs/homarr/issues/1723)) ([1e8a535](https://github.com/homarr-labs/homarr/commit/1e8a5359b78e28db7683ba837a962685446c6c32))
|
||||
* always require db-user and password for mysql ([#1730](https://github.com/homarr-labs/homarr/issues/1730)) ([d2584b6](https://github.com/homarr-labs/homarr/commit/d2584b63c55ca143a2b5dc6a71cb00fd5612b78a))
|
||||
* **deps:** update dependency tldts to ^6.1.69 ([#1726](https://github.com/homarr-labs/homarr/issues/1726)) ([c0f6816](https://github.com/homarr-labs/homarr/commit/c0f681637db1d6e7abdf3326b3a3c17283440d09))
|
||||
* **deps:** update dependency undici to v7.2.0 ([#1718](https://github.com/homarr-labs/homarr/issues/1718)) ([61afab5](https://github.com/homarr-labs/homarr/commit/61afab518f6193a700a547faffa4619b28fb0837))
|
||||
* **deps:** update dependency zod-form-data to ^2.0.3 ([#1717](https://github.com/homarr-labs/homarr/issues/1717)) ([a7bd664](https://github.com/homarr-labs/homarr/commit/a7bd664c5cee578574bf37b7149a2142be9f8b7a))
|
||||
* **deps:** update dependency zod-form-data to ^2.0.4 ([#1719](https://github.com/homarr-labs/homarr/issues/1719)) ([3b7e6cc](https://github.com/homarr-labs/homarr/commit/3b7e6cc083220a0e3a0cf4f5243c073067ba5bc7))
|
||||
* **deps:** update nextjs monorepo to ^14.2.21 ([#1727](https://github.com/homarr-labs/homarr/issues/1727)) ([2b4bc83](https://github.com/homarr-labs/homarr/commit/2b4bc8352efa1e2cffb71a6d4d6e42f941358f02))
|
||||
* **deps:** update tiptap monorepo to v2.10.4 ([#1729](https://github.com/homarr-labs/homarr/issues/1729)) ([2ae3d93](https://github.com/homarr-labs/homarr/commit/2ae3d938eeea5bd49a74d9c09b4cb2c680b059d0))
|
||||
* mysql operations not working ([#1728](https://github.com/homarr-labs/homarr/issues/1728)) ([550bca6](https://github.com/homarr-labs/homarr/commit/550bca6dee743a4ed68ec15677304ecfd7a9a385))
|
||||
* update of dev branch has no credentials ([#1724](https://github.com/homarr-labs/homarr/issues/1724)) ([bb7f751](https://github.com/homarr-labs/homarr/commit/bb7f7514814d49a77ac79f63d68923d08148573e))
|
||||
|
||||
## [1.0.0-beta.4](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2024-12-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* about version reads nextjs package-json ([#1706](https://github.com/homarr-labs/homarr/issues/1706)) ([d803087](https://github.com/homarr-labs/homarr/commit/d8030873452d2248d79d16a2fc4464ff2fbb478d))
|
||||
* conflicts between common admin group for ldap or oidc and credentials default group ([#1710](https://github.com/homarr-labs/homarr/issues/1710)) ([3c0f272](https://github.com/homarr-labs/homarr/commit/3c0f272707ee66fd00bffb0d16e30df98b4f1aff))
|
||||
* **deps:** update dependency ldapts to v7.3.0 ([#1696](https://github.com/homarr-labs/homarr/issues/1696)) ([6943505](https://github.com/homarr-labs/homarr/commit/694350583a0e53014a950c4a5bed4d487ad4ca7b))
|
||||
* **deps:** update dependency next-intl to v3.26.2 ([#1703](https://github.com/homarr-labs/homarr/issues/1703)) ([d3a883a](https://github.com/homarr-labs/homarr/commit/d3a883a252167612f3f64579730c8f26453897e3))
|
||||
* docker statup permission issue ([#1709](https://github.com/homarr-labs/homarr/issues/1709)) ([1baa9fc](https://github.com/homarr-labs/homarr/commit/1baa9fc74cf15df8ead7bbe6d1615f7bf0895813))
|
||||
* import sidebar issues ([#1712](https://github.com/homarr-labs/homarr/issues/1712)) ([a519994](https://github.com/homarr-labs/homarr/commit/a51999457b57c042b543768bc1ef43647d62a85d))
|
||||
* jellyfin integration does not allow username password auth ([#1714](https://github.com/homarr-labs/homarr/issues/1714)) ([63a7be3](https://github.com/homarr-labs/homarr/commit/63a7be35a4cf0f11610c2097afc32d2ced0ef9e1))
|
||||
* permission required for home page ([#1711](https://github.com/homarr-labs/homarr/issues/1711)) ([5fbbf03](https://github.com/homarr-labs/homarr/commit/5fbbf037ce8a06642533c7f68c1c34ed8b01e4e4))
|
||||
|
||||
## [1.0.0-beta.3](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2024-12-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* boolean checks do not work correctly in deploy workflow ([649506b](https://github.com/homarr-labs/homarr/commit/649506b56a5f3cbbee9b782b637730f2196785cc))
|
||||
|
||||
## [1.0.0-beta.2](https://github.com/homarr-labs/homarr/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2024-12-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+21
-30
@@ -25,51 +25,41 @@ RUN corepack enable pnpm && pnpm build
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# gettext is required for envsubst
|
||||
RUN apk add --no-cache redis nginx bash gettext
|
||||
# gettext is required for envsubst, openssl for generating AUTH_SECRET, su-exec for running application as non-root
|
||||
RUN apk add --no-cache redis nginx bash gettext su-exec openssl
|
||||
RUN mkdir /appdata
|
||||
VOLUME /appdata
|
||||
RUN mkdir /secrets
|
||||
VOLUME /secrets
|
||||
|
||||
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Enable homarr cli
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
|
||||
COPY --from=builder /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
|
||||
RUN echo $'#!/bin/bash\ncd /app/apps/cli && node ./cli.cjs "$@"' > /usr/bin/homarr
|
||||
RUN chmod +x /usr/bin/homarr
|
||||
|
||||
# Don't run production as root
|
||||
RUN chown -R nextjs:nodejs /appdata
|
||||
RUN chown -R nextjs:nodejs /secrets
|
||||
RUN mkdir -p /var/cache/nginx && chown -R nextjs:nodejs /var/cache/nginx && \
|
||||
mkdir -p /var/log/nginx && chown -R nextjs:nodejs /var/log/nginx && \
|
||||
mkdir -p /var/lib/nginx && chown -R nextjs:nodejs /var/lib/nginx && \
|
||||
touch /run/nginx/nginx.pid && chown -R nextjs:nodejs /run/nginx/nginx.pid && \
|
||||
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs && chown -R nextjs:nodejs /etc/nginx
|
||||
USER nextjs
|
||||
RUN mkdir -p /var/cache/nginx && \
|
||||
mkdir -p /var/log/nginx && \
|
||||
mkdir -p /var/lib/nginx && \
|
||||
touch /run/nginx/nginx.pid && \
|
||||
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs
|
||||
|
||||
COPY --from=builder /app/apps/nextjs/next.config.mjs .
|
||||
COPY --from=builder /app/apps/nextjs/package.json .
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
|
||||
COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
|
||||
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
|
||||
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/packages/db/migrations ./db/migrations
|
||||
COPY --from=builder /app/packages/db/migrations ./db/migrations
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/public
|
||||
COPY --chown=nextjs:nodejs scripts/run.sh ./run.sh
|
||||
COPY --chown=nextjs:nodejs scripts/generateRandomSecureKey.js ./generateRandomSecureKey.js
|
||||
COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf
|
||||
COPY --chown=nextjs:nodejs nginx.conf /etc/nginx/templates/nginx.conf
|
||||
COPY --from=builder /app/apps/nextjs/.next/standalone ./
|
||||
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
|
||||
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
|
||||
COPY scripts/run.sh ./run.sh
|
||||
COPY --chmod=777 scripts/entrypoint.sh ./entrypoint.sh
|
||||
COPY packages/redis/redis.conf /app/redis.conf
|
||||
COPY nginx.conf /etc/nginx/templates/nginx.conf
|
||||
|
||||
|
||||
ENV DB_URL='/appdata/db/db.sqlite'
|
||||
@@ -77,4 +67,5 @@ ENV DB_DIALECT='sqlite'
|
||||
ENV DB_DRIVER='better-sqlite3'
|
||||
ENV AUTH_PROVIDERS='credentials'
|
||||
|
||||
CMD ["sh", "run.sh"]
|
||||
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
||||
CMD ["sh", "run.sh"]
|
||||
@@ -1,5 +1,7 @@
|
||||
// Importing env files here to validate on build
|
||||
import "@homarr/auth/env.mjs";
|
||||
import "@homarr/db/env.mjs";
|
||||
import "@homarr/common/env.mjs";
|
||||
|
||||
import MillionLint from "@million/lint";
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
|
||||
+15
-15
@@ -38,18 +38,18 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@mantine/colors-generator": "^7.15.1",
|
||||
"@mantine/core": "^7.15.1",
|
||||
"@mantine/dropzone": "^7.15.1",
|
||||
"@mantine/hooks": "^7.15.1",
|
||||
"@mantine/modals": "^7.15.1",
|
||||
"@mantine/tiptap": "^7.15.1",
|
||||
"@mantine/colors-generator": "^7.15.2",
|
||||
"@mantine/core": "^7.15.2",
|
||||
"@mantine/dropzone": "^7.15.2",
|
||||
"@mantine/hooks": "^7.15.2",
|
||||
"@mantine/modals": "^7.15.2",
|
||||
"@mantine/tiptap": "^7.15.2",
|
||||
"@million/lint": "1.0.14",
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"@tabler/icons-react": "^3.26.0",
|
||||
"@tanstack/react-query": "^5.62.8",
|
||||
"@tanstack/react-query-devtools": "^5.62.8",
|
||||
"@tanstack/react-query-next-experimental": "5.62.8",
|
||||
"@tanstack/react-query": "^5.62.12",
|
||||
"@tanstack/react-query-devtools": "^5.62.12",
|
||||
"@tanstack/react-query-next-experimental": "5.62.12",
|
||||
"@trpc/client": "next",
|
||||
"@trpc/next": "next",
|
||||
"@trpc/react-query": "next",
|
||||
@@ -63,14 +63,14 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"flag-icons": "^7.2.3",
|
||||
"glob": "^11.0.0",
|
||||
"jotai": "^2.10.4",
|
||||
"jotai": "^2.11.0",
|
||||
"mantine-react-table": "2.0.0-beta.7",
|
||||
"next": "^14.2.20",
|
||||
"next": "^14.2.22",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.83.0",
|
||||
"superjson": "2.2.2",
|
||||
@@ -82,12 +82,12 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "2.4.5",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"concurrently": "^9.1.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.17.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.4.2",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Card, Center, Stack, Text, Title } from "@mantine/core";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { and, db, eq } from "@homarr/db";
|
||||
import { invites } from "@homarr/db/schema/sqlite";
|
||||
import { invites } from "@homarr/db/schema";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
|
||||
@@ -57,7 +57,7 @@ export default async function InviteUsagePage({ params, searchParams }: InviteUs
|
||||
{t("subtitle")}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Card bg="dark.8" w={64 * 6} maw="90vw">
|
||||
<Card withBorder w={64 * 6} maw="90vw">
|
||||
<RegistrationForm invite={invite} />
|
||||
</Card>
|
||||
<Text size="xs" c="gray.5" ta="center">
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async function Login({ searchParams }: LoginProps) {
|
||||
{t("subtitle")}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Card bg="dark.8" w={64 * 6} maw="90vw">
|
||||
<Card withBorder w={64 * 6} maw="90vw">
|
||||
<LoginForm
|
||||
providers={env.AUTH_PROVIDERS}
|
||||
oidcClientName={env.AUTH_OIDC_CLIENT_NAME}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IconHomeOff } from "@tabler/icons-react";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { db } from "@homarr/db";
|
||||
import { boards } from "@homarr/db/schema/sqlite";
|
||||
import { boards } from "@homarr/db/schema";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
|
||||
import type { BoardNotFoundProps } from "~/components/board/not-found";
|
||||
|
||||
@@ -24,10 +24,19 @@ export const ImportDropZone = ({ loading, updateFile }: ImportDropZoneProps) =>
|
||||
}}
|
||||
acceptColor="blue.6"
|
||||
rejectColor="red.6"
|
||||
accept={[MIME_TYPES.zip]}
|
||||
accept={[MIME_TYPES.zip, "application/x-zip-compressed"]}
|
||||
loading={loading}
|
||||
multiple={false}
|
||||
maxSize={1024 * 1024 * 1024 * 64} // 64 MB
|
||||
onReject={(rejections) => {
|
||||
console.error(
|
||||
"Rejected files",
|
||||
rejections.map(
|
||||
(rejection) =>
|
||||
`File: ${rejection.file.name} size=${rejection.file.size} fileType=${rejection.file.type}\n - ${rejection.errors.map((error) => error.message).join("\n - ")}`,
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: "none" }}>
|
||||
<Dropzone.Accept>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { IconBox, IconPencil } from "@tabler/icons-react";
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { parseAppHrefWithVariablesServer } from "@homarr/common/server";
|
||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
@@ -82,8 +81,8 @@ const AppCard = async ({ app }: AppCardProps) => {
|
||||
</Text>
|
||||
)}
|
||||
{app.href && (
|
||||
<Anchor href={parseAppHrefWithVariablesServer(app.href)} lineClamp={1} size="sm" w="min-content">
|
||||
{parseAppHrefWithVariablesServer(app.href)}
|
||||
<Anchor href={app.href} lineClamp={1} size="sm" w="min-content">
|
||||
{app.href}
|
||||
</Anchor>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Affix, Button, Group, Menu } from "@mantine/core";
|
||||
import { Affix, Button, Menu } from "@mantine/core";
|
||||
import { IconCategoryPlus, IconChevronDown, IconFileImport } from "@tabler/icons-react";
|
||||
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { AddBoardModal, ImportBoardModal } from "@homarr/modals-collection";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { BetaBadge } from "@homarr/ui";
|
||||
|
||||
export const CreateBoardButton = () => {
|
||||
const t = useI18n();
|
||||
@@ -26,10 +25,7 @@ export const CreateBoardButton = () => {
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item onClick={openImportModal} leftSection={<IconFileImport size="1rem" />}>
|
||||
<Group>
|
||||
{t("board.action.oldImport.label")}
|
||||
<BetaBadge size="xs" />
|
||||
</Group>
|
||||
{t("board.action.oldImport.label")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import { useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Alert, Button, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { Alert, Button, Checkbox, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions, getIntegrationName } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions, getIntegrationName, integrationDefs } from "@homarr/definitions";
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
|
||||
@@ -38,6 +38,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
kind,
|
||||
value: "",
|
||||
})),
|
||||
attemptSearchEngineCreation: true,
|
||||
},
|
||||
});
|
||||
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
|
||||
@@ -78,6 +79,8 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
);
|
||||
};
|
||||
|
||||
const supportsSearchEngine = integrationDefs[searchParams.kind].category.flat().includes("search");
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((value) => void handleSubmitAsync(value))}>
|
||||
<Stack>
|
||||
@@ -104,6 +107,16 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
|
||||
{supportsSearchEngine && (
|
||||
<Checkbox
|
||||
label={t("integration.field.attemptSearchEngineCreation.label")}
|
||||
description={t("integration.field.attemptSearchEngineCreation.description", {
|
||||
kind: getIntegrationName(searchParams.kind),
|
||||
})}
|
||||
{...form.getInputProps("attemptSearchEngineCreation", { type: "checkbox" })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Group justify="end" align="center">
|
||||
<Button variant="default" component={Link} href="/manage/integrations">
|
||||
{t("common.action.backToOverview")}
|
||||
|
||||
@@ -13,10 +13,7 @@ interface CopyMediaProps {
|
||||
export const CopyMedia = ({ media }: CopyMediaProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
const url =
|
||||
typeof window !== "undefined"
|
||||
? `${window.location.protocol}://${window.location.hostname}:${window.location.port}/api/user-medias/${media.id}`
|
||||
: "";
|
||||
const url = typeof window !== "undefined" ? `${window.location.origin}/api/user-medias/${media.id}` : "";
|
||||
|
||||
return (
|
||||
<CopyButton value={url}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { notFound } from "next/navigation";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { db, inArray } from "@homarr/db";
|
||||
import { groups } from "@homarr/db/schema/sqlite";
|
||||
import { groups } from "@homarr/db/schema";
|
||||
import { everyoneGroup } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { hashPasswordAsync } from "@homarr/auth";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createSessionAsync } from "@homarr/auth/server";
|
||||
import { db, eq } from "@homarr/db";
|
||||
import { apiKeys } from "@homarr/db/schema/sqlite";
|
||||
import { apiKeys } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
const handlerAsync = async (req: NextRequest) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { db, eq } from "@homarr/db";
|
||||
import { medias } from "@homarr/db/schema/sqlite";
|
||||
import { medias } from "@homarr/db/schema";
|
||||
|
||||
export async function GET(_req: NextRequest, { params }: { params: { id: string } }) {
|
||||
const image = await db.query.medias.findFirst({
|
||||
|
||||
@@ -10,6 +10,12 @@ import { useSectionContext } from "./section-context";
|
||||
export const SectionContent = () => {
|
||||
const { section, innerSections, refs } = useSectionContext();
|
||||
const board = useRequiredBoard();
|
||||
|
||||
/**
|
||||
* IMPORTANT: THE ORDER OF THE BELOW ITEMS HAS TO MATCH THE ORDER OF
|
||||
* THE ITEMS RENDERED WITH GRIDSTACK, OTHERWISE THE ITEMS WILL BE MIXED UP
|
||||
* @see https://github.com/homarr-labs/homarr/pull/1770
|
||||
*/
|
||||
const sortedItems = useMemo(() => {
|
||||
return [
|
||||
...section.items.map((item) => ({ ...item, type: "item" as const })),
|
||||
@@ -19,7 +25,7 @@ export const SectionContent = () => {
|
||||
return itemA.xOffset - itemB.xOffset;
|
||||
}
|
||||
|
||||
return itemA.yOffset - itemB.xOffset;
|
||||
return itemA.yOffset - itemB.yOffset;
|
||||
});
|
||||
}, [section.items, innerSections]);
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
--background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
|
||||
--border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
|
||||
}
|
||||
background-color: var(--background-color);
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--background-color) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
@@ -7,13 +7,13 @@ import { CurrentUserAvatar } from "~/components/user-avatar";
|
||||
import { UserAvatarMenu } from "~/components/user-avatar-menu";
|
||||
|
||||
export const UserButton = async () => {
|
||||
const data = await api.updateChecker.getAvailableUpdates();
|
||||
const session = await auth();
|
||||
const isAdmin = session?.user.permissions.includes("admin");
|
||||
const data = isAdmin ? await api.updateChecker.getAvailableUpdates() : undefined;
|
||||
return (
|
||||
<UserAvatarMenu availableUpdates={isAdmin ? data : undefined}>
|
||||
<UserAvatarMenu availableUpdates={data}>
|
||||
<UnstyledButton>
|
||||
<Indicator disabled={data.length === 0 || !isAdmin} size={15} processing withBorder>
|
||||
<Indicator disabled={data?.length === 0 || !isAdmin} size={15} processing withBorder>
|
||||
<CurrentUserAvatar size="md" />
|
||||
</Indicator>
|
||||
</UnstyledButton>
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
const isUsingDbUrl = Boolean(process.env.DB_URL);
|
||||
const isUsingDbHost = Boolean(process.env.DB_HOST);
|
||||
const isUsingDbCredentials = process.env.DB_DRIVER === "mysql2";
|
||||
|
||||
export const env = createEnv({
|
||||
shared: {
|
||||
VERCEL_URL: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((url) => (url ? `https://${url}` : undefined)),
|
||||
PORT: z.coerce.number().default(3000),
|
||||
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
||||
},
|
||||
@@ -19,21 +11,6 @@ export const env = createEnv({
|
||||
* built with invalid env vars.
|
||||
*/
|
||||
server: {
|
||||
DB_DRIVER: z.enum(["better-sqlite3", "mysql2"]).default("better-sqlite3"),
|
||||
// If the DB_HOST is set, the DB_URL is optional
|
||||
DB_URL: isUsingDbHost ? z.string().optional() : z.string(),
|
||||
DB_HOST: isUsingDbUrl ? z.string().optional() : z.string(),
|
||||
DB_PORT: isUsingDbUrl
|
||||
? z.string().regex(/\d+/).transform(Number).optional()
|
||||
: z
|
||||
.string()
|
||||
.regex(/\d+/)
|
||||
.transform(Number)
|
||||
.refine((number) => number >= 1)
|
||||
.default("3306"),
|
||||
DB_USER: isUsingDbCredentials ? z.string() : z.string().optional(),
|
||||
DB_PASSWORD: isUsingDbCredentials ? z.string() : z.string().optional(),
|
||||
DB_NAME: isUsingDbUrl ? z.string().optional() : z.string(),
|
||||
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
|
||||
DOCKER_HOSTNAMES: z.string().optional(),
|
||||
DOCKER_PORTS: z.number().optional(),
|
||||
@@ -49,15 +26,7 @@ export const env = createEnv({
|
||||
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
|
||||
*/
|
||||
runtimeEnv: {
|
||||
VERCEL_URL: process.env.VERCEL_URL,
|
||||
PORT: process.env.PORT,
|
||||
DB_URL: process.env.DB_URL,
|
||||
DB_HOST: process.env.DB_HOST,
|
||||
DB_USER: process.env.DB_USER,
|
||||
DB_PASSWORD: process.env.DB_PASSWORD,
|
||||
DB_NAME: process.env.DB_NAME,
|
||||
DB_PORT: process.env.DB_PORT,
|
||||
DB_DRIVER: process.env.DB_DRIVER,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
|
||||
DOCKER_PORTS: process.env.DOCKER_PORTS,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fsPromises from "fs/promises";
|
||||
import { glob } from "glob";
|
||||
|
||||
import packageJson from "~/../package.json";
|
||||
import packageJson from "../../../../package.json";
|
||||
|
||||
const getPackageVersion = () => packageJson.version;
|
||||
const getDependenciesAsync = async (): Promise<PackageJsonDependencies> => {
|
||||
|
||||
@@ -38,14 +38,14 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.1.1"
|
||||
"undici": "7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
"@types/node": "^22.10.5",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.17.0",
|
||||
"prettier": "^3.4.2",
|
||||
"tsx": "4.19.2",
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@
|
||||
<a href="https://github.com/homarr-labs/homarr/actions/workflows/deployment-docker-image.yml">
|
||||
<img title="Docker CI Status" src="https://github.com/homarr-labs/homarr/actions/workflows/deployment-docker-image.yml/badge.svg" alt="CI Status">
|
||||
</a>
|
||||
<a href=https://translate.homarr.dev/>
|
||||
<img title="Translations" src="https://badges.crowdin.net/homarr/localized.svg" />
|
||||
<a href="https://crowdin.com/project/homarr_labs">
|
||||
<img title="Translations" src="https://badges.crowdin.net/homarr_labs/localized.svg" />
|
||||
</a>
|
||||
<a href="https://discord.gg/aCsmEV5RgA">
|
||||
<img title="Discord" src="https://discordapp.com/api/guilds/972958686051962910/widget.png?style=shield">
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { chromium } from "playwright";
|
||||
import { GenericContainer } from "testcontainers";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { OnboardingActions } from "./shared/actions/onboarding-actions";
|
||||
import { createHomarrContainer, withLogs } from "./shared/create-homarr-container";
|
||||
import { createSqliteDbFileAsync } from "./shared/e2e-db";
|
||||
|
||||
const defaultCredentials = {
|
||||
username: "admin",
|
||||
password: "password",
|
||||
email: "admin@homarr.dev",
|
||||
group: "lldap_admin",
|
||||
};
|
||||
|
||||
const ldapBase = "dc=example,dc=com";
|
||||
|
||||
describe("LLDAP authorization", () => {
|
||||
test("Authorize with LLDAP successfully", async () => {
|
||||
// Arrange
|
||||
const lldapContainer = await createLldapContainer().start();
|
||||
const { db, localMountPath } = await createSqliteDbFileAsync();
|
||||
const homarrContainer = await createHomarrContainer({
|
||||
environment: {
|
||||
AUTH_PROVIDERS: "ldap",
|
||||
AUTH_LDAP_URI: `ldap://host.docker.internal:${lldapContainer.getMappedPort(3890)}`,
|
||||
AUTH_LDAP_BASE: ldapBase,
|
||||
AUTH_LDAP_BIND_DN: `uid=${defaultCredentials.username},ou=People,${ldapBase}`,
|
||||
AUTH_LDAP_BIND_PASSWORD: defaultCredentials.password,
|
||||
},
|
||||
mounts: {
|
||||
"/appdata": localMountPath,
|
||||
},
|
||||
}).start();
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
const onboardingActions = new OnboardingActions(page, db);
|
||||
await onboardingActions.skipOnboardingAsync({
|
||||
group: defaultCredentials.group,
|
||||
});
|
||||
|
||||
// Act
|
||||
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`);
|
||||
await page.getByLabel("Username").fill(defaultCredentials.username);
|
||||
await page.getByLabel("Password").fill(defaultCredentials.password);
|
||||
await page.locator("css=button[type='submit']").click();
|
||||
|
||||
// Assert
|
||||
await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
|
||||
const users = await db.query.users.findMany({
|
||||
with: {
|
||||
groups: {
|
||||
with: {
|
||||
group: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(users).toHaveLength(1);
|
||||
const user = users[0]!;
|
||||
expect(user).toEqual(
|
||||
expect.objectContaining({
|
||||
name: defaultCredentials.username,
|
||||
email: defaultCredentials.email,
|
||||
provider: "ldap",
|
||||
}),
|
||||
);
|
||||
|
||||
const groups = user.groups.map((g) => g.group.name);
|
||||
expect(groups).toContain(defaultCredentials.group);
|
||||
|
||||
// Cleanup
|
||||
await browser.close();
|
||||
await homarrContainer.stop();
|
||||
await lldapContainer.stop();
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
const createLldapContainer = () => {
|
||||
return withLogs(
|
||||
new GenericContainer("lldap/lldap:stable").withExposedPorts(3890).withEnvironment({
|
||||
LLDAP_JWT_SECRET: "REPLACE_WITH_RANDOM",
|
||||
LLDAP_KEY_SEED: "REPLACE_WITH_RANDOM",
|
||||
LLDAP_LDAP_BASE_DN: ldapBase,
|
||||
LLDAP_LDAP_USER_PASS: defaultCredentials.password,
|
||||
LLDAP_LDAP_USER_EMAIL: defaultCredentials.email,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { chromium } from "playwright";
|
||||
import { describe, test } from "vitest";
|
||||
|
||||
import { OnboardingActions } from "./shared/actions/onboarding-actions";
|
||||
import { OnboardingAssertions } from "./shared/assertions/onboarding-assertions";
|
||||
import { createHomarrContainer } from "./shared/create-homarr-container";
|
||||
import { createSqliteDbFileAsync } from "./shared/e2e-db";
|
||||
|
||||
describe("Onboarding", () => {
|
||||
test("Credentials onboarding should be successful", async () => {
|
||||
// Arrange
|
||||
const { db, localMountPath } = await createSqliteDbFileAsync();
|
||||
const homarrContainer = await createHomarrContainer({
|
||||
mounts: {
|
||||
"/appdata": localMountPath,
|
||||
},
|
||||
}).start();
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const actions = new OnboardingActions(page, db);
|
||||
const assertions = new OnboardingAssertions(page, db);
|
||||
|
||||
// Act
|
||||
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
|
||||
await actions.startOnboardingAsync("scratch");
|
||||
await actions.processUserStepAsync({
|
||||
username: "admin",
|
||||
password: "Comp(exP4sswOrd",
|
||||
confirmPassword: "Comp(exP4sswOrd",
|
||||
});
|
||||
await actions.processSettingsStepAsync();
|
||||
|
||||
// Assert
|
||||
await assertions.assertFinishStepVisibleAsync();
|
||||
await assertions.assertUserAndAdminGroupInsertedAsync("admin");
|
||||
await assertions.assertDbOnboardingStepAsync("finish");
|
||||
|
||||
// Cleanup
|
||||
await browser.close();
|
||||
await homarrContainer.stop();
|
||||
}, 60_000);
|
||||
|
||||
test("External provider onboarding setup should be successful", async () => {
|
||||
// Arrange
|
||||
const { db, localMountPath } = await createSqliteDbFileAsync();
|
||||
const homarrContainer = await createHomarrContainer({
|
||||
environment: {
|
||||
AUTH_PROVIDERS: "ldap",
|
||||
AUTH_LDAP_URI: "ldap://host.docker.internal:3890",
|
||||
AUTH_LDAP_BASE: "",
|
||||
AUTH_LDAP_BIND_DN: "",
|
||||
AUTH_LDAP_BIND_PASSWORD: "",
|
||||
},
|
||||
mounts: {
|
||||
"/appdata": localMountPath,
|
||||
},
|
||||
}).start();
|
||||
const externalGroupName = "oidc-admins";
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const actions = new OnboardingActions(page, db);
|
||||
const assertions = new OnboardingAssertions(page, db);
|
||||
|
||||
// Act
|
||||
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
|
||||
await actions.startOnboardingAsync("scratch");
|
||||
await actions.processExternalGroupStepAsync({
|
||||
name: externalGroupName,
|
||||
});
|
||||
await actions.processSettingsStepAsync();
|
||||
|
||||
// Assert
|
||||
await assertions.assertFinishStepVisibleAsync();
|
||||
await assertions.assertExternalGroupInsertedAsync(externalGroupName);
|
||||
await assertions.assertDbOnboardingStepAsync("finish");
|
||||
|
||||
// Cleanup
|
||||
await browser.close();
|
||||
await homarrContainer.stop();
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import type { Page } from "playwright";
|
||||
|
||||
import * as sqliteSchema from "../../../packages/db/schema/sqlite";
|
||||
import type { SqliteDatabase } from "../e2e-db";
|
||||
|
||||
export class OnboardingActions {
|
||||
private readonly page: Page;
|
||||
private readonly db: SqliteDatabase;
|
||||
|
||||
constructor(page: Page, db: SqliteDatabase) {
|
||||
this.page = page;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public async skipOnboardingAsync(input?: { group?: string }) {
|
||||
await this.db.update(sqliteSchema.onboarding).set({
|
||||
step: "finish",
|
||||
});
|
||||
|
||||
if (input?.group) {
|
||||
await this.db.insert(sqliteSchema.groups).values({
|
||||
id: createId(),
|
||||
name: input.group,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async startOnboardingAsync(type: "scratch" | "before 1.0") {
|
||||
await this.page.locator("button", { hasText: type }).click();
|
||||
}
|
||||
|
||||
public async processUserStepAsync(input: { username: string; password: string; confirmPassword: string }) {
|
||||
await this.page.waitForSelector("text=administrator user");
|
||||
|
||||
await this.page.getByLabel("Username").fill(input.username);
|
||||
await this.page.getByLabel("Password", { exact: true }).fill(input.password);
|
||||
await this.page.getByLabel("Confirm password").fill(input.confirmPassword);
|
||||
|
||||
await this.page.locator("css=button[type='submit']").click();
|
||||
}
|
||||
|
||||
public async processExternalGroupStepAsync(input: { name: string }) {
|
||||
await this.page.waitForSelector("text=external provider");
|
||||
await this.page.locator("input").fill(input.name);
|
||||
await this.page.locator("css=button[type='submit']").click();
|
||||
}
|
||||
|
||||
public async processSettingsStepAsync() {
|
||||
await this.page.waitForSelector("text=Analytics");
|
||||
await this.page.locator("css=button[type='submit']").click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { Page } from "playwright";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import * as sqliteSchema from "../../../packages/db/schema/sqlite";
|
||||
import { OnboardingStep } from "../../../packages/definitions/src";
|
||||
import { credentialsAdminGroup } from "../../../packages/definitions/src/group";
|
||||
import type { SqliteDatabase } from "../e2e-db";
|
||||
|
||||
export class OnboardingAssertions {
|
||||
private readonly page: Page;
|
||||
private readonly db: SqliteDatabase;
|
||||
|
||||
constructor(page: Page, db: SqliteDatabase) {
|
||||
this.page = page;
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public async assertDbOnboardingStepAsync(expectedStep: OnboardingStep) {
|
||||
const onboarding = await this.db.query.onboarding.findFirst();
|
||||
expect(onboarding?.step).toEqual(expectedStep);
|
||||
}
|
||||
|
||||
public async assertUserAndAdminGroupInsertedAsync(expectedUsername: string) {
|
||||
const users = await this.db.query.users.findMany({
|
||||
with: {
|
||||
groups: {
|
||||
with: {
|
||||
group: {
|
||||
with: {
|
||||
permissions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const user = users.find((u) => u.name === expectedUsername);
|
||||
expect(user).toBeDefined();
|
||||
|
||||
const adminGroup = user!.groups.find((g) => g.group.name === credentialsAdminGroup);
|
||||
expect(adminGroup).toBeDefined();
|
||||
expect(adminGroup!.group.permissions).toEqual([expect.objectContaining({ permission: "admin" })]);
|
||||
}
|
||||
|
||||
public async assertExternalGroupInsertedAsync(expectedGroupName: string) {
|
||||
const group = await this.db.query.groups.findFirst({
|
||||
where: eq(sqliteSchema.groups.name, expectedGroupName),
|
||||
with: {
|
||||
permissions: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(group).toBeDefined();
|
||||
expect(group!.permissions).toEqual([expect.objectContaining({ permission: "admin" })]);
|
||||
}
|
||||
|
||||
public async assertFinishStepVisibleAsync() {
|
||||
await this.page.waitForSelector("text=completed the setup", { timeout: 5000 });
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,51 @@
|
||||
import { GenericContainer, Wait } from "testcontainers";
|
||||
import { Environment } from "testcontainers/build/types";
|
||||
|
||||
export const createHomarrContainer = () => {
|
||||
export const createHomarrContainer = (
|
||||
options: {
|
||||
environment?: Environment;
|
||||
mounts?: {
|
||||
"/appdata"?: string;
|
||||
"/var/run/docker.sock"?: string;
|
||||
};
|
||||
} = {},
|
||||
) => {
|
||||
if (!process.env.CI) {
|
||||
throw new Error("This test should only be run in CI or with a homarr image named 'homarr-e2e'");
|
||||
}
|
||||
|
||||
return new GenericContainer("homarr-e2e")
|
||||
const container = new GenericContainer("homarr-e2e")
|
||||
.withExposedPorts(7575)
|
||||
.withWaitStrategy(Wait.forHttp("/api/health/ready", 7575));
|
||||
.withEnvironment({
|
||||
...options.environment,
|
||||
SECRET_ENCRYPTION_KEY: "0".repeat(64),
|
||||
})
|
||||
.withBindMounts(
|
||||
Object.entries(options.mounts ?? {})
|
||||
.filter((item) => item?.[0] !== undefined)
|
||||
.map(([container, local]) => ({
|
||||
source: local,
|
||||
target: container,
|
||||
})),
|
||||
)
|
||||
.withWaitStrategy(Wait.forHttp("/api/health/ready", 7575))
|
||||
.withExtraHosts([
|
||||
{
|
||||
// This enabled the usage of host.docker.internal as hostname in the container
|
||||
host: "host.docker.internal",
|
||||
ipAddress: "host-gateway",
|
||||
},
|
||||
]);
|
||||
|
||||
return withLogs(container);
|
||||
};
|
||||
|
||||
export const withLogs = (container: GenericContainer) => {
|
||||
container.withLogConsumer((stream) =>
|
||||
stream
|
||||
.on("data", (line) => console.log(line))
|
||||
.on("err", (line) => console.error(line))
|
||||
.on("end", () => console.log("Stream closed")),
|
||||
);
|
||||
return container;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { mkdir } from "fs/promises";
|
||||
import path from "path";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import Database from "better-sqlite3";
|
||||
import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
|
||||
import * as sqliteSchema from "../../packages/db/schema/sqlite";
|
||||
|
||||
export const createSqliteDbFileAsync = async () => {
|
||||
const localMountPath = path.join(__dirname, "tmp", createId());
|
||||
await mkdir(path.join(localMountPath, "db"), { recursive: true });
|
||||
|
||||
const localDbUrl = path.join(localMountPath, "db", "db.sqlite");
|
||||
|
||||
const connection = new Database(localDbUrl);
|
||||
const db = drizzle(connection, {
|
||||
schema: sqliteSchema,
|
||||
casing: "snake_case",
|
||||
});
|
||||
|
||||
await migrate(db, {
|
||||
migrationsFolder: path.join(__dirname, "..", "..", "packages", "db", "migrations", "sqlite"),
|
||||
});
|
||||
|
||||
return {
|
||||
db,
|
||||
localMountPath,
|
||||
};
|
||||
};
|
||||
|
||||
export type SqliteDatabase = BetterSQLite3Database<typeof sqliteSchema>;
|
||||
+1
-1
@@ -21,7 +21,7 @@ http {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.0.0-beta.2",
|
||||
"version": "1.0.0-beta.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -32,11 +32,11 @@
|
||||
"devDependencies": {
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/commit-analyzer": "^13.0.0",
|
||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/github": "^11.0.1",
|
||||
"@semantic-release/npm": "^12.0.1",
|
||||
"@semantic-release/release-notes-generator": "^14.0.2",
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@turbo/gen": "^2.3.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^2.1.8",
|
||||
@@ -45,14 +45,14 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"jsdom": "^25.0.1",
|
||||
"prettier": "^3.4.2",
|
||||
"semantic-release": "^24.2.0",
|
||||
"semantic-release": "^24.2.1",
|
||||
"testcontainers": "^10.16.0",
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "^5.7.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"packageManager": "pnpm@9.15.2",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Umami } from "@umami/node";
|
||||
|
||||
import { count, db } from "@homarr/db";
|
||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import { integrations, items, users } from "@homarr/db/schema/sqlite";
|
||||
import { integrations, items, users } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { defaultServerSettings } from "@homarr/server-settings";
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/icons": "workspace:^0.1.0",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/old-import": "workspace:^0.1.0",
|
||||
@@ -40,16 +41,17 @@
|
||||
"@trpc/react-query": "next",
|
||||
"@trpc/server": "next",
|
||||
"dockerode": "^4.0.2",
|
||||
"next": "^14.2.20",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "^14.2.22",
|
||||
"react": "^19.0.0",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^2.1.0"
|
||||
"trpc-to-openapi": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/dockerode": "^3.3.32",
|
||||
"@types/dockerode": "^3.3.33",
|
||||
"eslint": "^9.17.0",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.7.2"
|
||||
|
||||
@@ -16,7 +16,6 @@ export const fetchApi = createTRPCClient<AppRouter>({
|
||||
|
||||
function getBaseUrl() {
|
||||
if (typeof window !== "undefined") return window.location.origin;
|
||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { decryptSecret } from "@homarr/common/server";
|
||||
import type { AtLeastOneOf } from "@homarr/common/types";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, eq, inArray } from "@homarr/db";
|
||||
import { integrations } from "@homarr/db/schema/sqlite";
|
||||
import { integrations } from "@homarr/db/schema";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { items } from "@homarr/db/schema/sqlite";
|
||||
import { items } from "@homarr/db/schema";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||
import { generateSecureRandomToken } from "@homarr/common/server";
|
||||
import { createId, db } from "@homarr/db";
|
||||
import { apiKeys } from "@homarr/db/schema/sqlite";
|
||||
import { apiKeys } from "@homarr/db/schema";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { asc, createId, eq, inArray, like } from "@homarr/db";
|
||||
import { apps } from "@homarr/db/schema/sqlite";
|
||||
import { apps } from "@homarr/db/schema";
|
||||
import { selectAppSchema } from "@homarr/db/validationSchemas";
|
||||
import { validation, z } from "@homarr/validation";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import SuperJSON from "superjson";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { db, eq, or } from "@homarr/db";
|
||||
import { items } from "@homarr/db/schema/sqlite";
|
||||
import { items } from "@homarr/db/schema";
|
||||
|
||||
import type { WidgetComponentProps } from "../../../../widgets/src";
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
items,
|
||||
sections,
|
||||
users,
|
||||
} from "@homarr/db/schema/sqlite";
|
||||
} from "@homarr/db/schema";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
|
||||
import { importOldmarrAsync } from "@homarr/old-import";
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Session } from "@homarr/auth";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import type { Database, SQL } from "@homarr/db";
|
||||
import { eq, inArray } from "@homarr/db";
|
||||
import { boardGroupPermissions, boardUserPermissions, groupMembers } from "@homarr/db/schema/sqlite";
|
||||
import { boardGroupPermissions, boardUserPermissions, groupMembers } from "@homarr/db/schema";
|
||||
import type { BoardPermission } from "@homarr/definitions";
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ import type Docker from "dockerode";
|
||||
import type { Container } from "dockerode";
|
||||
|
||||
import { db, like, or } from "@homarr/db";
|
||||
import { icons } from "@homarr/db/schema/sqlite";
|
||||
import { icons } from "@homarr/db/schema";
|
||||
import type { DockerContainerState } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
import { createCacheChannel } from "@homarr/redis";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq, like, not, sql } from "@homarr/db";
|
||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema/sqlite";
|
||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
|
||||
import { everyoneGroup } from "@homarr/definitions";
|
||||
import { validation, z } from "@homarr/validation";
|
||||
|
||||
@@ -152,11 +152,17 @@ export const groupRouter = createTRPCRouter({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
||||
|
||||
const groupId = createId();
|
||||
await ctx.db.insert(groups).values({
|
||||
id: createId(),
|
||||
id: groupId,
|
||||
name: input.name,
|
||||
});
|
||||
|
||||
await ctx.db.insert(groupPermissions).values({
|
||||
groupId,
|
||||
permission: "admin",
|
||||
});
|
||||
|
||||
await nextOnboardingStepAsync(ctx.db, undefined);
|
||||
}),
|
||||
createGroup: permissionRequiredProcedure
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AnySQLiteTable } from "drizzle-orm/sqlite-core";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { count } from "@homarr/db";
|
||||
import { apps, boards, groups, integrations, invites, users } from "@homarr/db/schema/sqlite";
|
||||
import { apps, boards, groups, integrations, invites, users } from "@homarr/db/schema";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { count, like } from "@homarr/db";
|
||||
import { icons } from "@homarr/db/schema/sqlite";
|
||||
import { and, count, like } from "@homarr/db";
|
||||
import { icons } from "@homarr/db/schema";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
@@ -15,7 +15,11 @@ export const iconsRouter = createTRPCRouter({
|
||||
name: true,
|
||||
url: true,
|
||||
},
|
||||
where: (input.searchText?.length ?? 0) > 0 ? like(icons.name, `%${input.searchText}%`) : undefined,
|
||||
where:
|
||||
(input.searchText?.length ?? 0) > 0
|
||||
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
and(...input.searchText!.split(" ").map((keyword) => like(icons.name, `%${keyword}%`)))
|
||||
: undefined,
|
||||
limit: input.limitPerGroup,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Session } from "@homarr/auth";
|
||||
import { constructIntegrationPermissions } from "@homarr/auth/shared";
|
||||
import type { Database, SQL } from "@homarr/db";
|
||||
import { eq, inArray } from "@homarr/db";
|
||||
import { groupMembers, integrationGroupPermissions, integrationUserPermissions } from "@homarr/db/schema/sqlite";
|
||||
import { groupMembers, integrationGroupPermissions, integrationUserPermissions } from "@homarr/db/schema";
|
||||
import type { IntegrationPermission } from "@homarr/definitions";
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,18 +11,21 @@ import {
|
||||
integrations,
|
||||
integrationSecrets,
|
||||
integrationUserPermissions,
|
||||
} from "@homarr/db/schema/sqlite";
|
||||
searchEngines,
|
||||
} from "@homarr/db/schema";
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import {
|
||||
getIconUrl,
|
||||
getIntegrationKindsByCategory,
|
||||
getPermissionsWithParents,
|
||||
integrationDefs,
|
||||
integrationKinds,
|
||||
integrationSecretKindObject,
|
||||
isIntegrationWithSearchSupport,
|
||||
} from "@homarr/definitions";
|
||||
import { integrationCreatorFromSecrets } from "@homarr/integrations";
|
||||
import { integrationCreator } from "@homarr/integrations";
|
||||
import { validation, z } from "@homarr/validation";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
import { throwIfActionForbiddenAsync } from "./integration-access";
|
||||
import { testConnectionAsync } from "./integration-test-connection";
|
||||
@@ -90,7 +93,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
where: inArray(
|
||||
integrations.kind,
|
||||
objectEntries(integrationDefs)
|
||||
.filter(([_, integration]) => integration.supportsSearch)
|
||||
.filter(([_, integration]) => [...integration.category].includes("search"))
|
||||
.map(([kind, _]) => kind),
|
||||
),
|
||||
});
|
||||
@@ -191,6 +194,18 @@ export const integrationRouter = createTRPCRouter({
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (input.attemptSearchEngineCreation) {
|
||||
const icon = getIconUrl(input.kind);
|
||||
await ctx.db.insert(searchEngines).values({
|
||||
id: createId(),
|
||||
name: input.name,
|
||||
integrationId,
|
||||
type: "fromIntegration",
|
||||
iconUrl: icon,
|
||||
short: await getNextValidShortNameForSearchEngineAsync(ctx.db, input.name),
|
||||
});
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||
@@ -383,31 +398,11 @@ export const integrationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
searchInIntegration: protectedProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search")))
|
||||
.input(z.object({ integrationId: z.string(), query: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.integrationId),
|
||||
with: {
|
||||
secrets: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!integration) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "The requested integration does not exist",
|
||||
});
|
||||
}
|
||||
|
||||
if (!isIntegrationWithSearchSupport(integration)) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "The requested integration does not support searching",
|
||||
});
|
||||
}
|
||||
|
||||
const integrationInstance = integrationCreatorFromSecrets(integration);
|
||||
return await integrationInstance.searchAsync(input.query);
|
||||
const integrationInstance = integrationCreator(ctx.integration);
|
||||
return await integrationInstance.searchAsync(encodeURI(input.query));
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -430,6 +425,36 @@ interface AddSecretInput {
|
||||
value: string;
|
||||
kind: IntegrationSecretKind;
|
||||
}
|
||||
|
||||
const getNextValidShortNameForSearchEngineAsync = async (db: Database, integrationName: string) => {
|
||||
const searchEngines = await db.query.searchEngines.findMany({
|
||||
columns: {
|
||||
short: true,
|
||||
},
|
||||
});
|
||||
|
||||
const usedShortNames = searchEngines.flatMap((searchEngine) => searchEngine.short.toLowerCase());
|
||||
const nameByIntegrationName = integrationName.slice(0, 1).toLowerCase();
|
||||
|
||||
if (!usedShortNames.includes(nameByIntegrationName)) {
|
||||
return nameByIntegrationName;
|
||||
}
|
||||
|
||||
// 8 is max length constraint
|
||||
for (let i = 2; i < 9999999; i++) {
|
||||
const generatedName = `${nameByIntegrationName}${i}`;
|
||||
if (usedShortNames.includes(generatedName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return generatedName;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Unable to automatically generate a short name. All possible variations were exhausted. Please disable the automatic creation and choose one later yourself.",
|
||||
);
|
||||
};
|
||||
|
||||
const addSecretAsync = async (db: Database, input: AddSecretInput) => {
|
||||
await db.insert(integrationSecrets).values({
|
||||
kind: input.kind,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { decryptSecret } from "@homarr/common/server";
|
||||
import type { Integration } from "@homarr/db/schema/sqlite";
|
||||
import type { Integration } from "@homarr/db/schema";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions } from "@homarr/definitions";
|
||||
import { integrationCreator, IntegrationTestConnectionError } from "@homarr/integrations";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { randomBytes } from "crypto";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { asc, createId, eq } from "@homarr/db";
|
||||
import { invites } from "@homarr/db/schema/sqlite";
|
||||
import { invites } from "@homarr/db/schema";
|
||||
import { selectInviteSchema } from "@homarr/db/validationSchemas";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { InferInsertModel } from "@homarr/db";
|
||||
import { and, createId, desc, eq, like } from "@homarr/db";
|
||||
import { medias } from "@homarr/db/schema/sqlite";
|
||||
import { iconRepositories, icons, medias } from "@homarr/db/schema";
|
||||
import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local";
|
||||
import { validation, z } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
|
||||
@@ -52,13 +54,29 @@ export const mediaRouter = createTRPCRouter({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const content = Buffer.from(await input.file.arrayBuffer());
|
||||
const id = createId();
|
||||
await ctx.db.insert(medias).values({
|
||||
const media = {
|
||||
id,
|
||||
creatorId: ctx.session.user.id,
|
||||
content,
|
||||
size: input.file.size,
|
||||
contentType: input.file.type,
|
||||
name: input.file.name,
|
||||
} satisfies InferInsertModel<typeof medias>;
|
||||
await ctx.db.insert(medias).values(media);
|
||||
|
||||
const localIconRepository = await ctx.db.query.iconRepositories.findFirst({
|
||||
where: eq(iconRepositories.slug, LOCAL_ICON_REPOSITORY_SLUG),
|
||||
});
|
||||
|
||||
if (!localIconRepository) return id;
|
||||
|
||||
const icon = mapMediaToIcon(media);
|
||||
await ctx.db.insert(icons).values({
|
||||
id: createId(),
|
||||
checksum: icon.checksum,
|
||||
name: icon.fileNameWithExtension,
|
||||
url: icon.imageUrl,
|
||||
iconRepositoryId: localIconRepository.id,
|
||||
});
|
||||
|
||||
return id;
|
||||
@@ -67,6 +85,7 @@ export const mediaRouter = createTRPCRouter({
|
||||
const dbMedia = await ctx.db.query.medias.findFirst({
|
||||
where: eq(medias.id, input.id),
|
||||
columns: {
|
||||
id: true,
|
||||
creatorId: true,
|
||||
},
|
||||
});
|
||||
@@ -87,5 +106,6 @@ export const mediaRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
await ctx.db.delete(medias).where(eq(medias.id, input.id));
|
||||
await ctx.db.delete(icons).where(eq(icons.url, createLocalImageUrl(input.id)));
|
||||
}),
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { objectEntries } from "@homarr/common";
|
||||
import type { MaybePromise } from "@homarr/common/types";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { eq } from "@homarr/db";
|
||||
import { groups, onboarding } from "@homarr/db/schema/sqlite";
|
||||
import { groups, onboarding } from "@homarr/db/schema";
|
||||
import type { OnboardingStep } from "@homarr/definitions";
|
||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { onboarding } from "@homarr/db/schema/sqlite";
|
||||
import { onboarding } from "@homarr/db/schema";
|
||||
import { onboardingSteps } from "@homarr/definitions";
|
||||
import { z, zodEnumFromArray } from "@homarr/validation";
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { createId, eq, like, sql } from "@homarr/db";
|
||||
import { searchEngines } from "@homarr/db/schema/sqlite";
|
||||
import { searchEngines } from "@homarr/db/schema";
|
||||
import { integrationCreator } from "@homarr/integrations";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
|
||||
|
||||
export const searchEngineRouter = createTRPCRouter({
|
||||
@@ -56,9 +58,32 @@ export const searchEngineRouter = createTRPCRouter({
|
||||
search: protectedProcedure.input(validation.common.search).query(async ({ ctx, input }) => {
|
||||
return await ctx.db.query.searchEngines.findMany({
|
||||
where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`),
|
||||
with: {
|
||||
integration: {
|
||||
columns: {
|
||||
kind: true,
|
||||
url: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
limit: input.limit,
|
||||
});
|
||||
}),
|
||||
getMediaRequestOptions: protectedProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
|
||||
.input(validation.common.mediaRequestOptions)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const integration = integrationCreator(ctx.integration);
|
||||
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
||||
}),
|
||||
requestMedia: protectedProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
|
||||
.input(validation.common.requestMedia)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const integration = integrationCreator(ctx.integration);
|
||||
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
|
||||
}),
|
||||
create: permissionRequiredProcedure
|
||||
.requiresPermission("search-engine-create")
|
||||
.input(validation.searchEngine.manage)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createId } from "@homarr/db";
|
||||
import { apps } from "@homarr/db/schema/sqlite";
|
||||
import { apps } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
sections,
|
||||
serverSettings,
|
||||
users,
|
||||
} from "@homarr/db/schema/sqlite";
|
||||
} from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import type { BoardPermission, GroupPermissionKey } from "@homarr/definitions";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import * as authShared from "@homarr/auth/shared";
|
||||
import { createId, eq } from "@homarr/db";
|
||||
import { boards, users } from "@homarr/db/schema/sqlite";
|
||||
import { boards, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { throwIfActionForbiddenAsync } from "../../board/board-access";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import * as env from "@homarr/auth/env.mjs";
|
||||
import { createId, eq } from "@homarr/db";
|
||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema/sqlite";
|
||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import * as authShared from "@homarr/auth/shared";
|
||||
import { createId, eq } from "@homarr/db";
|
||||
import { integrations, users } from "@homarr/db/schema/sqlite";
|
||||
import { integrations, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { throwIfActionForbiddenAsync } from "../../integration/integration-access";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { encryptSecret } from "@homarr/common/server";
|
||||
import { createId } from "@homarr/db";
|
||||
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
||||
import { integrations, integrationSecrets } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
|
||||
@@ -179,6 +179,7 @@ describe("create should create a new integration", () => {
|
||||
kind: "jellyfin" as const,
|
||||
url: "http://jellyfin.local",
|
||||
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
|
||||
attemptSearchEngineCreation: false,
|
||||
};
|
||||
|
||||
const fakeNow = new Date("2023-07-01T00:00:00Z");
|
||||
@@ -201,6 +202,48 @@ describe("create should create a new integration", () => {
|
||||
expect(dbSecret!.updatedAt).toEqual(fakeNow);
|
||||
});
|
||||
|
||||
test("with create integration access should create a new integration when creating search engine", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
session: defaultSessionWithPermissions(["integration-create"]),
|
||||
});
|
||||
const input = {
|
||||
name: "Jellyseerr",
|
||||
kind: "jellyseerr" as const,
|
||||
url: "http://jellyseerr.local",
|
||||
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
|
||||
attemptSearchEngineCreation: true,
|
||||
};
|
||||
|
||||
const fakeNow = new Date("2023-07-01T00:00:00Z");
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(fakeNow);
|
||||
await caller.create(input);
|
||||
vi.useRealTimers();
|
||||
|
||||
const dbIntegration = await db.query.integrations.findFirst();
|
||||
const dbSecret = await db.query.integrationSecrets.findFirst();
|
||||
const dbSearchEngine = await db.query.searchEngines.findFirst();
|
||||
expect(dbIntegration).toBeDefined();
|
||||
expect(dbIntegration!.name).toBe(input.name);
|
||||
expect(dbIntegration!.kind).toBe(input.kind);
|
||||
expect(dbIntegration!.url).toBe(input.url);
|
||||
|
||||
expect(dbSecret!.integrationId).toBe(dbIntegration!.id);
|
||||
expect(dbSecret).toBeDefined();
|
||||
expect(dbSecret!.kind).toBe(input.secrets[0]!.kind);
|
||||
expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||
expect(dbSecret!.updatedAt).toEqual(fakeNow);
|
||||
|
||||
expect(dbSearchEngine!.integrationId).toBe(dbIntegration!.id);
|
||||
expect(dbSearchEngine!.short).toBe("j");
|
||||
expect(dbSearchEngine!.name).toBe(input.name);
|
||||
expect(dbSearchEngine!.iconUrl).toBe(
|
||||
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyseerr.png",
|
||||
);
|
||||
});
|
||||
|
||||
test("without create integration access should throw permission error", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
@@ -213,6 +256,7 @@ describe("create should create a new integration", () => {
|
||||
kind: "jellyfin" as const,
|
||||
url: "http://jellyfin.local",
|
||||
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
|
||||
attemptSearchEngineCreation: false,
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createId } from "@homarr/db";
|
||||
import { invites, users } from "@homarr/db/schema/sqlite";
|
||||
import { invites, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { inviteRouter } from "../invite";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createId } from "@homarr/db";
|
||||
import { serverSettings } from "@homarr/db/schema/sqlite";
|
||||
import { serverSettings } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { describe, expect, it, test, vi } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { createId, eq, schema } from "@homarr/db";
|
||||
import { onboarding, users } from "@homarr/db/schema/sqlite";
|
||||
import { createId, eq } from "@homarr/db";
|
||||
import { invites, onboarding, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions";
|
||||
|
||||
@@ -121,7 +121,7 @@ describe("register should create a user with valid invitation", () => {
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
});
|
||||
await db.insert(schema.invites).values({
|
||||
await db.insert(invites).values({
|
||||
id: inviteId,
|
||||
token: inviteToken,
|
||||
creatorId: userId,
|
||||
@@ -176,7 +176,7 @@ describe("register should create a user with valid invitation", () => {
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
});
|
||||
await db.insert(schema.invites).values({
|
||||
await db.insert(invites).values({
|
||||
id: inviteId,
|
||||
token: inviteToken,
|
||||
creatorId: userId,
|
||||
@@ -211,7 +211,7 @@ describe("editProfile shoud update user", () => {
|
||||
|
||||
const emailVerified = new Date(2024, 0, 5);
|
||||
|
||||
await db.insert(schema.users).values({
|
||||
await db.insert(users).values({
|
||||
id: defaultOwnerId,
|
||||
name: "TEST 1",
|
||||
email: "abc@gmail.com",
|
||||
@@ -226,7 +226,7 @@ describe("editProfile shoud update user", () => {
|
||||
});
|
||||
|
||||
// assert
|
||||
const user = await db.select().from(schema.users).where(eq(schema.users.id, defaultOwnerId));
|
||||
const user = await db.select().from(users).where(eq(users.id, defaultOwnerId));
|
||||
|
||||
expect(user).toHaveLength(1);
|
||||
expect(user[0]).containSubset({
|
||||
@@ -245,7 +245,7 @@ describe("editProfile shoud update user", () => {
|
||||
session: defaultSession,
|
||||
});
|
||||
|
||||
await db.insert(schema.users).values({
|
||||
await db.insert(users).values({
|
||||
id: defaultOwnerId,
|
||||
name: "TEST 1",
|
||||
email: "abc@gmail.com",
|
||||
@@ -260,7 +260,7 @@ describe("editProfile shoud update user", () => {
|
||||
});
|
||||
|
||||
// assert
|
||||
const user = await db.select().from(schema.users).where(eq(schema.users.id, defaultOwnerId));
|
||||
const user = await db.select().from(users).where(eq(users.id, defaultOwnerId));
|
||||
|
||||
expect(user).toHaveLength(1);
|
||||
expect(user[0]).containSubset({
|
||||
@@ -295,11 +295,11 @@ describe("delete should delete user", () => {
|
||||
},
|
||||
];
|
||||
|
||||
await db.insert(schema.users).values(initialUsers);
|
||||
await db.insert(users).values(initialUsers);
|
||||
|
||||
await caller.delete({ userId: defaultOwnerId });
|
||||
|
||||
const usersInDb = await db.select().from(schema.users);
|
||||
const usersInDb = await db.select().from(users);
|
||||
expect(usersInDb).toHaveLength(2);
|
||||
expect(usersInDb[0]).containSubset(initialUsers[0]);
|
||||
expect(usersInDb[1]).containSubset(initialUsers[2]);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq, like, schema } from "@homarr/db";
|
||||
import { groupMembers, groupPermissions, groups, invites, users } from "@homarr/db/schema/sqlite";
|
||||
import { and, createId, eq, like } from "@homarr/db";
|
||||
import { groupMembers, groupPermissions, groups, invites, users } from "@homarr/db/schema";
|
||||
import { selectUserSchema } from "@homarr/db/validationSchemas";
|
||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
@@ -509,7 +509,7 @@ const createUserAsync = async (db: Database, input: Omit<z.infer<typeof validati
|
||||
const username = input.username.toLowerCase();
|
||||
|
||||
const userId = createId();
|
||||
await db.insert(schema.users).values({
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
name: username,
|
||||
email: input.email,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import type { Integration } from "@homarr/db/schema/sqlite";
|
||||
import type { Integration } from "@homarr/db/schema";
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { integrationCreator } from "@homarr/integrations";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import type { Integration } from "@homarr/db/schema/sqlite";
|
||||
import type { Integration } from "@homarr/db/schema";
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
|
||||
|
||||
@@ -8,6 +8,7 @@ import { indexerManagerRouter } from "./indexer-manager";
|
||||
import { mediaRequestsRouter } from "./media-requests";
|
||||
import { mediaServerRouter } from "./media-server";
|
||||
import { mediaTranscodingRouter } from "./media-transcoding";
|
||||
import { minecraftRouter } from "./minecraft";
|
||||
import { notebookRouter } from "./notebook";
|
||||
import { rssFeedRouter } from "./rssFeed";
|
||||
import { smartHomeRouter } from "./smart-home";
|
||||
@@ -27,4 +28,5 @@ export const widgetRouter = createTRPCRouter({
|
||||
indexerManager: indexerManagerRouter,
|
||||
healthMonitoring: healthMonitoringRouter,
|
||||
mediaTranscoding: mediaTranscodingRouter,
|
||||
minecraft: minecraftRouter,
|
||||
});
|
||||
@@ -21,6 +21,7 @@ export const mediaServerRouter = createTRPCRouter({
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationKind: integration.kind,
|
||||
sessions: data,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { MinecraftServerStatus } from "@homarr/request-handler/minecraft-server-status";
|
||||
import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
const serverStatusInputSchema = z.object({
|
||||
domain: z.string().nonempty(),
|
||||
isBedrockServer: z.boolean(),
|
||||
});
|
||||
export const minecraftRouter = createTRPCRouter({
|
||||
getServerStatus: publicProcedure.input(serverStatusInputSchema).query(async ({ input }) => {
|
||||
const innerHandler = minecraftServerStatusRequestHandler.handler({
|
||||
isBedrockServer: input.isBedrockServer,
|
||||
domain: input.domain,
|
||||
});
|
||||
return await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
|
||||
}),
|
||||
subscribeServerStatus: publicProcedure.input(serverStatusInputSchema).subscription(({ input }) => {
|
||||
return observable<MinecraftServerStatus>((emit) => {
|
||||
const innerHandler = minecraftServerStatusRequestHandler.handler({
|
||||
isBedrockServer: input.isBedrockServer,
|
||||
domain: input.domain,
|
||||
});
|
||||
const unsubscribe = innerHandler.subscribe((data) => {
|
||||
emit.next(data);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import { eq } from "@homarr/db";
|
||||
import { items } from "@homarr/db/schema/sqlite";
|
||||
import { items } from "@homarr/db/schema";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "../../trpc";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { accounts, sessions, users } from "@homarr/db/schema/sqlite";
|
||||
import { accounts, sessions, users } from "@homarr/db/schema";
|
||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
|
||||
export const createAdapter = (db: Database, provider: SupportedAuthProvider | "unknown"): Adapter => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { NextAuthConfig } from "next-auth";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { eq, inArray } from "@homarr/db";
|
||||
import { groupMembers, groupPermissions, users } from "@homarr/db/schema/sqlite";
|
||||
import { groupMembers, groupPermissions, users } from "@homarr/db/schema";
|
||||
import { getPermissionsWithChildren } from "@homarr/definitions";
|
||||
|
||||
export const getCurrentUserPermissionsAsync = async (db: Database, userId: string) => {
|
||||
|
||||
@@ -64,7 +64,6 @@ export const env = createEnv({
|
||||
server: {
|
||||
AUTH_LOGOUT_REDIRECT_URL: z.string().url().optional(),
|
||||
AUTH_SESSION_EXPIRY_TIME: createDurationSchema("30d"),
|
||||
AUTH_SECRET: process.env.NODE_ENV === "production" ? z.string().min(1) : z.string().min(1).optional(),
|
||||
AUTH_PROVIDERS: authProvidersSchema,
|
||||
...(authProviders.includes("oidc")
|
||||
? {
|
||||
@@ -98,7 +97,6 @@ export const env = createEnv({
|
||||
runtimeEnv: {
|
||||
AUTH_LOGOUT_REDIRECT_URL: process.env.AUTH_LOGOUT_REDIRECT_URL,
|
||||
AUTH_SESSION_EXPIRY_TIME: process.env.AUTH_SESSION_EXPIRY_TIME,
|
||||
AUTH_SECRET: process.env.AUTH_SECRET,
|
||||
AUTH_PROVIDERS: process.env.AUTH_PROVIDERS,
|
||||
AUTH_LDAP_BASE: process.env.AUTH_LDAP_BASE,
|
||||
AUTH_LDAP_BIND_DN: process.env.AUTH_LDAP_BIND_DN,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { NextAuthConfig } from "next-auth";
|
||||
|
||||
import { and, eq, inArray } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { groupMembers, groups, users } from "@homarr/db/schema/sqlite";
|
||||
import { groupMembers, groups, users } from "@homarr/db/schema";
|
||||
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cookies": "^0.9.1",
|
||||
"ldapts": "7.2.2",
|
||||
"next": "^14.2.20",
|
||||
"ldapts": "7.3.0",
|
||||
"next": "^14.2.22",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Session } from "next-auth";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, eq, inArray, or } from "@homarr/db";
|
||||
import { boards, boardUserPermissions, groupMembers } from "@homarr/db/schema/sqlite";
|
||||
import { boards, boardUserPermissions, groupMembers } from "@homarr/db/schema";
|
||||
import type { IntegrationPermission } from "@homarr/definitions";
|
||||
|
||||
import { constructIntegrationPermissions } from "./integration-permissions";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
import { db, eq, inArray } from "@homarr/db";
|
||||
import { groupMembers, integrationGroupPermissions, integrationUserPermissions } from "@homarr/db/schema/sqlite";
|
||||
import { groupMembers, integrationGroupPermissions, integrationUserPermissions } from "@homarr/db/schema";
|
||||
|
||||
import { constructIntegrationPermissions } from "./integration-permissions";
|
||||
|
||||
|
||||
@@ -3,14 +3,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { InferInsertModel } from "@homarr/db";
|
||||
import { createId } from "@homarr/db";
|
||||
import {
|
||||
boardGroupPermissions,
|
||||
boards,
|
||||
boardUserPermissions,
|
||||
groupMembers,
|
||||
groups,
|
||||
users,
|
||||
} from "@homarr/db/schema/sqlite";
|
||||
import { boardGroupPermissions, boards, boardUserPermissions, groupMembers, groups, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import * as integrationPermissions from "../integration-permissions";
|
||||
|
||||
@@ -2,7 +2,7 @@ import bcrypt from "bcrypt";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema/sqlite";
|
||||
import { users } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { validation, z } from "@homarr/validation";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CredentialsSignin } from "@auth/core/errors";
|
||||
|
||||
import type { Database, InferInsertModel } from "@homarr/db";
|
||||
import { and, createId, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema/sqlite";
|
||||
import { users } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { validation } from "@homarr/validation";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
@@ -23,7 +23,8 @@ export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig<Profil
|
||||
authorization: {
|
||||
params: {
|
||||
scope: env.AUTH_OIDC_SCOPE_OVERWRITE,
|
||||
redirect_uri: createRedirectUri(headers, "/api/auth/callback/oidc"),
|
||||
// We fallback to https as generally oidc providers require https
|
||||
redirect_uri: createRedirectUri(headers, "/api/auth/callback/oidc", "https"),
|
||||
},
|
||||
},
|
||||
profile(profile) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { createId } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema/sqlite";
|
||||
import { users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { createSaltAsync, hashPasswordAsync } from "../../security";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, createId, eq } from "@homarr/db";
|
||||
import { groups, users } from "@homarr/db/schema/sqlite";
|
||||
import { groups, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { authorizeWithLdapCredentialsAsync } from "../credentials/authorization/ldap-authorization";
|
||||
|
||||
@@ -8,12 +8,16 @@ import { extractBaseUrlFromHeaders } from "@homarr/common";
|
||||
* @param pathname
|
||||
* @returns
|
||||
*/
|
||||
export const createRedirectUri = (headers: ReadonlyHeaders | null, pathname: string) => {
|
||||
export const createRedirectUri = (
|
||||
headers: ReadonlyHeaders | null,
|
||||
pathname: string,
|
||||
fallbackProtocol: "http" | "https" = "http",
|
||||
) => {
|
||||
if (!headers) {
|
||||
return pathname;
|
||||
}
|
||||
|
||||
const baseUrl = extractBaseUrlFromHeaders(headers);
|
||||
const baseUrl = extractBaseUrlFromHeaders(headers, fallbackProtocol);
|
||||
|
||||
const path = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { users } from "@homarr/db/schema/sqlite";
|
||||
import { users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { createAdapter } from "../adapter";
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AdapterUser } from "@auth/core/adapters";
|
||||
import type { JWT } from "next-auth/jwt";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema/sqlite";
|
||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import * as definitions from "@homarr/definitions";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { eq } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { groupMembers, groups, users } from "@homarr/db/schema/sqlite";
|
||||
import { groupMembers, groups, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { command, string } from "@drizzle-team/brocli";
|
||||
import { hashPasswordAsync } from "@homarr/auth";
|
||||
import { generateSecureRandomToken } from "@homarr/common/server";
|
||||
import { and, db, eq } from "@homarr/db";
|
||||
import { sessions, users } from "@homarr/db/schema/sqlite";
|
||||
import { sessions, users } from "@homarr/db/schema";
|
||||
|
||||
export const resetPassword = command({
|
||||
name: "reset-password",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { randomBytes } from "crypto";
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
const errorSuffix = `, please generate a 64 character secret in hex format or use the following: "${randomBytes(32).toString("hex")}"`;
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
SECRET_ENCRYPTION_KEY: z
|
||||
.string({
|
||||
required_error: `SECRET_ENCRYPTION_KEY is required${errorSuffix}`,
|
||||
})
|
||||
.min(64, {
|
||||
message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`,
|
||||
})
|
||||
.max(64, {
|
||||
message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`,
|
||||
})
|
||||
.regex(/^[0-9a-fA-F]{64}$/, {
|
||||
message: `SECRET_ENCRYPTION_KEY must only contain hex characters${errorSuffix}`,
|
||||
}),
|
||||
},
|
||||
runtimeEnv: {
|
||||
SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY,
|
||||
},
|
||||
skipValidation:
|
||||
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint",
|
||||
});
|
||||
@@ -8,7 +8,8 @@
|
||||
".": "./index.ts",
|
||||
"./types": "./src/types.ts",
|
||||
"./server": "./src/server.ts",
|
||||
"./client": "./src/client.ts"
|
||||
"./client": "./src/client.ts",
|
||||
"./env.mjs": "./env.mjs"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
@@ -27,9 +28,8 @@
|
||||
"dependencies": {
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "^14.2.20",
|
||||
"react": "^19.0.0",
|
||||
"tldts": "^6.1.68"
|
||||
"next": "^14.2.22",
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import * as tldts from "tldts";
|
||||
|
||||
const safeParseTldts = (url: string) => {
|
||||
try {
|
||||
return tldts.parse(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseAppHrefWithVariables = <TInput extends string | null>(url: TInput, currentHref: string): TInput => {
|
||||
if (!url || url.length === 0) return url;
|
||||
|
||||
const tldtsResult = safeParseTldts(currentHref);
|
||||
|
||||
const urlObject = new URL(currentHref);
|
||||
|
||||
return url
|
||||
.replaceAll("[homarr_base]", `${urlObject.protocol}//${urlObject.hostname}`)
|
||||
.replaceAll("[homarr_hostname]", tldtsResult?.hostname ?? "")
|
||||
.replaceAll("[homarr_domain]", tldtsResult?.domain ?? "")
|
||||
.replaceAll("[homarr_protocol]", urlObject.protocol.replace(":", "")) as TInput;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { parseAppHrefWithVariables } from "./base";
|
||||
|
||||
export const parseAppHrefWithVariablesClient = <TInput extends string | null>(url: TInput): TInput => {
|
||||
if (typeof window === "undefined") return url;
|
||||
return parseAppHrefWithVariables(url, window.location.href);
|
||||
};
|
||||
Loaded 100 of 248 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user