Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b332baa75 | ||
|
|
d0b9cbf155 | ||
|
|
3948f7f9a4 | ||
|
|
092c95bbe0 | ||
|
|
0fc7ca7738 | ||
|
|
da34b0a3cf | ||
|
|
75100d1a49 | ||
|
|
bd5dcb991c | ||
|
|
b064304201 | ||
|
|
c01d9a4dc5 | ||
|
|
a821bd71f4 | ||
|
|
17d92727f2 | ||
|
|
8ef2661be4 | ||
|
|
df212cf511 | ||
|
|
85f8d23da3 | ||
|
|
275224ee68 | ||
|
|
ffd1005ff1 | ||
|
|
bf046a715c | ||
|
|
c960c6fc79 | ||
|
|
75ba3f2ae7 | ||
|
|
c1cd563048 | ||
|
|
7caad6fc47 | ||
|
|
e07b9e6a88 | ||
|
|
14bff5d530 | ||
|
|
3e88ecd64a | ||
|
|
c6102edff4 | ||
|
|
646e2c12e6 | ||
|
|
95e7136284 | ||
|
|
9de9aa59a4 | ||
|
|
ee12461452 | ||
|
|
b03941ed80 | ||
|
|
968a82baf5 | ||
|
|
ea8bf43d7b | ||
|
|
964368a06c | ||
|
|
98ecc683e5 | ||
|
|
29ff879dfd | ||
|
|
674253262b | ||
|
|
f9aeb8ffe3 | ||
|
|
76b8b35cc1 | ||
|
|
8886380f0a | ||
|
|
0b41967ec3 | ||
|
|
1b0504198b | ||
|
|
3363eebddd | ||
|
|
85672d9cf1 | ||
|
|
3131082c77 | ||
|
|
f33b1cb5d0 | ||
|
|
4f32f11d8a | ||
|
|
466d0e0053 | ||
|
|
7c4b71af3d | ||
|
|
81d1231d8f | ||
|
|
858fe6e19a | ||
|
|
ff5c31342f | ||
|
|
807997ea76 | ||
|
|
9c12b18c6c | ||
|
|
3951d052de | ||
|
|
5975b53212 | ||
|
|
cc9522d981 | ||
|
|
7f77130978 | ||
|
|
243a97d637 | ||
|
|
4feeb8b173 | ||
|
|
b1a8012bc7 | ||
|
|
22b67188bc | ||
|
|
dd6e2c0f72 | ||
|
|
9db0016252 | ||
|
|
3e3505176a | ||
|
|
3687c4b3d6 | ||
|
|
944010a7b1 | ||
|
|
db13fd03c0 | ||
|
|
0b8a0098bd | ||
|
|
9a77b540bb | ||
|
|
c598579716 | ||
|
|
59b1630532 | ||
|
|
77044e8e0b | ||
|
|
0ec8f23b7c | ||
|
|
c69e331ded | ||
|
|
4273cd9fcd | ||
|
|
98515312a2 | ||
|
|
d1b14aca8b | ||
|
|
c6b4c9cbf2 | ||
|
|
19029f40b8 | ||
|
|
b6bc257726 | ||
|
|
fef50a53b1 | ||
|
|
94ff401d7d | ||
|
|
63328d125a | ||
|
|
457b970d8f | ||
|
|
8d69970562 | ||
|
|
8a071f9ab6 | ||
|
|
76b67ac8e1 |
No files matched your search
@@ -31,6 +31,9 @@ body:
|
||||
label: Version
|
||||
description: What version of Homarr are you running?
|
||||
options:
|
||||
- 1.14.0
|
||||
- 1.13.1
|
||||
- 1.13.0
|
||||
- 1.12.0
|
||||
- 1.11.0
|
||||
- 1.10.0
|
||||
|
||||
+121
@@ -1,3 +1,124 @@
|
||||
## [1.15.0](https://github.com/homarr-labs/homarr/compare/v1.14.0...v1.15.0) (2025-04-09)
|
||||
|
||||
### Features
|
||||
|
||||
* **boards:** add upload buttons for background, favicon and logo ([#2770](https://github.com/homarr-labs/homarr/issues/2770)) ([e07b9e6](https://github.com/homarr-labs/homarr/commit/e07b9e6a88ff0aad0aa5dae2f32f648ad5f0c0a8))
|
||||
* unifi controller integration ([#2236](https://github.com/homarr-labs/homarr/issues/2236)) ([c1cd563](https://github.com/homarr-labs/homarr/commit/c1cd56304872b5f7b2b49624c6f5018f750d126b))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @kubernetes/client-node to ^1.1.2 ([#2825](https://github.com/homarr-labs/homarr/issues/2825)) ([b064304](https://github.com/homarr-labs/homarr/commit/b064304201ed2ce52162edd5bc9ab5131da7ac97))
|
||||
* **deps:** update dependency ldapts to v7.4.0 ([#2815](https://github.com/homarr-labs/homarr/issues/2815)) ([275224e](https://github.com/homarr-labs/homarr/commit/275224ee68bb49f329a6765095b46ec249027aca))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.7 ([#2816](https://github.com/homarr-labs/homarr/issues/2816)) ([85f8d23](https://github.com/homarr-labs/homarr/commit/85f8d23da3f3ec17945d79ef8a5212c5ceeefe75))
|
||||
* **deps:** update dependency typescript-eslint to ^8.29.1 ([#2821](https://github.com/homarr-labs/homarr/issues/2821)) ([17d9272](https://github.com/homarr-labs/homarr/commit/17d92727f24cf0e2491dc93fd7c72759a24bbd54))
|
||||
* **deps:** update mantine monorepo to ^7.17.4 ([#2824](https://github.com/homarr-labs/homarr/issues/2824)) ([bd5dcb9](https://github.com/homarr-labs/homarr/commit/bd5dcb991c8bd476cefbae1b8b2f407dd8db1acb))
|
||||
* **deps:** update nextjs monorepo to v15.2.5 ([#2828](https://github.com/homarr-labs/homarr/issues/2828)) ([0fc7ca7](https://github.com/homarr-labs/homarr/commit/0fc7ca7738aee2d86940f43e3593368d4102cc73))
|
||||
* **deps:** update tanstack-query monorepo to ^5.72.0 ([#2817](https://github.com/homarr-labs/homarr/issues/2817)) ([df212cf](https://github.com/homarr-labs/homarr/commit/df212cf5114a970ad4aed84811cfc7a2e4be67b3))
|
||||
* **deps:** update tanstack-query monorepo to ^5.72.1 ([#2823](https://github.com/homarr-labs/homarr/issues/2823)) ([c01d9a4](https://github.com/homarr-labs/homarr/commit/c01d9a4dc5aba540a2b333a94507d027e5fb8d99))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.24.1 ([#2831](https://github.com/homarr-labs/homarr/issues/2831)) ([092c95b](https://github.com/homarr-labs/homarr/commit/092c95bbe0be73ad3dedb13c7f84ba7c36ca14db))
|
||||
* **deps:** update trpc monorepo to ^11.0.3 ([#2822](https://github.com/homarr-labs/homarr/issues/2822)) ([a821bd7](https://github.com/homarr-labs/homarr/commit/a821bd71f47e03a276a96af705bec90a00229e0f))
|
||||
* **deps:** update trpc monorepo to ^11.0.4 ([#2827](https://github.com/homarr-labs/homarr/issues/2827)) ([75100d1](https://github.com/homarr-labs/homarr/commit/75100d1a4926b838682206189eac046d844dba13))
|
||||
* **medias:** upload size limited by nginx to 1mb ([#2813](https://github.com/homarr-labs/homarr/issues/2813)) ([bf046a7](https://github.com/homarr-labs/homarr/commit/bf046a715cd6897792b2f1582d076c055063bf57))
|
||||
* **performance:** remove turbopack flag to no longer go out of memory after ([#2803](https://github.com/homarr-labs/homarr/issues/2803)) ([c6102ed](https://github.com/homarr-labs/homarr/commit/c6102edff467082803769c488ce642500edcdd25))
|
||||
* **security:** missing authorization check for changes to notebook widget ([#2832](https://github.com/homarr-labs/homarr/issues/2832)) ([3948f7f](https://github.com/homarr-labs/homarr/commit/3948f7f9a47cd4562c9b4fd8252f116b8a95c982))
|
||||
|
||||
## [1.14.0](https://github.com/homarr-labs/homarr/compare/v1.13.1...v1.14.0) (2025-04-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **cli:** add cli command to recreate credentials admin ([#2779](https://github.com/homarr-labs/homarr/issues/2779)) ([6742532](https://github.com/homarr-labs/homarr/commit/674253262bd4853ebde85d229d666a38733a7026))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @ctrl/qbittorrent to ^9.5.2 ([#2795](https://github.com/homarr-labs/homarr/issues/2795)) ([ea8bf43](https://github.com/homarr-labs/homarr/commit/ea8bf43d7b56f526fbf2d4a3955c2c179e4e74d8))
|
||||
* **deps:** update dependency @kubernetes/client-node to ^1.1.1 ([#2793](https://github.com/homarr-labs/homarr/issues/2793)) ([76b8b35](https://github.com/homarr-labs/homarr/commit/76b8b35cc1f586797210bdcd3d848d64b5f36d63))
|
||||
* **deps:** update dependency dockerode to ^4.0.5 ([#2757](https://github.com/homarr-labs/homarr/issues/2757)) ([db13fd0](https://github.com/homarr-labs/homarr/commit/db13fd03c018cb2f79f0b094b93b69d69fde97ff))
|
||||
* **deps:** update dependency eslint-plugin-react to ^7.37.5 ([#2794](https://github.com/homarr-labs/homarr/issues/2794)) ([f9aeb8f](https://github.com/homarr-labs/homarr/commit/f9aeb8ffe3ff4cd65cd76c560a687912262a735f))
|
||||
* **deps:** update dependency recharts to ^2.15.2 ([#2785](https://github.com/homarr-labs/homarr/issues/2785)) ([f33b1cb](https://github.com/homarr-labs/homarr/commit/f33b1cb5d0c317df5348e87e0f5f43fdc6044615))
|
||||
* **deps:** update dependency sass to ^1.86.1 ([#2762](https://github.com/homarr-labs/homarr/issues/2762)) ([3e35051](https://github.com/homarr-labs/homarr/commit/3e3505176a7df01bae5173d7f62380fa17859e8f))
|
||||
* **deps:** update dependency sass to ^1.86.2 ([#2784](https://github.com/homarr-labs/homarr/issues/2784)) ([3131082](https://github.com/homarr-labs/homarr/commit/3131082c778191be5568a93bbf5f10219f3c2be9))
|
||||
* **deps:** update dependency sass to ^1.86.3 ([#2796](https://github.com/homarr-labs/homarr/issues/2796)) ([98ecc68](https://github.com/homarr-labs/homarr/commit/98ecc683e563475644530682f08d335ac3671a5e))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.3 ([#2777](https://github.com/homarr-labs/homarr/issues/2777)) ([81d1231](https://github.com/homarr-labs/homarr/commit/81d1231d8fc026dd9f1ba58e28937aba9d94d88e))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.4 ([#2786](https://github.com/homarr-labs/homarr/issues/2786)) ([85672d9](https://github.com/homarr-labs/homarr/commit/85672d9cf15bf48c9cc2fb4e746f65814f46361b))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.5 ([#2787](https://github.com/homarr-labs/homarr/issues/2787)) ([3363eeb](https://github.com/homarr-labs/homarr/commit/3363eebdddf10a028d1cdebcb4d581105ba1d63c))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.6 ([#2798](https://github.com/homarr-labs/homarr/issues/2798)) ([968a82b](https://github.com/homarr-labs/homarr/commit/968a82baf5d6adc64c680508493b77b892fdd5fc))
|
||||
* **deps:** update dependency trpc-to-openapi to ^2.1.4 ([#2748](https://github.com/homarr-labs/homarr/issues/2748)) ([c69e331](https://github.com/homarr-labs/homarr/commit/c69e331ded3cfde837dcf28c8b1ba4a9b7a3c8aa))
|
||||
* **deps:** update dependency trpc-to-openapi to ^2.1.5 ([#2799](https://github.com/homarr-labs/homarr/issues/2799)) ([b03941e](https://github.com/homarr-labs/homarr/commit/b03941ed80057154f0b4b04c764652ada1fdf18c))
|
||||
* **deps:** update dependency typescript-eslint to ^8.29.0 ([#2761](https://github.com/homarr-labs/homarr/issues/2761)) ([3687c4b](https://github.com/homarr-labs/homarr/commit/3687c4b3d655d9cebb85fb3c3ad80f494e9a8f0a))
|
||||
* **deps:** update dependency undici to v7.7.0 ([#2776](https://github.com/homarr-labs/homarr/issues/2776)) ([ff5c313](https://github.com/homarr-labs/homarr/commit/ff5c31342fe9eb668533d7d9f0d65ad3f01eba0a))
|
||||
* **deps:** update tanstack-query monorepo to ^5.71.0 ([#2752](https://github.com/homarr-labs/homarr/issues/2752)) ([77044e8](https://github.com/homarr-labs/homarr/commit/77044e8e0b8e7267b8547eb1d0f86026948b292a))
|
||||
* **deps:** update tanstack-query monorepo to ^5.71.1 ([#2756](https://github.com/homarr-labs/homarr/issues/2756)) ([0b8a009](https://github.com/homarr-labs/homarr/commit/0b8a0098bd02a71107af9702e39000227028ec20))
|
||||
* **deps:** update tanstack-query monorepo to ^5.71.10 ([#2790](https://github.com/homarr-labs/homarr/issues/2790)) ([964368a](https://github.com/homarr-labs/homarr/commit/964368a06c8385df63a252e2d0759a2524348ac6))
|
||||
* **deps:** update tanstack-query monorepo to ^5.71.3 ([#2767](https://github.com/homarr-labs/homarr/issues/2767)) ([858fe6e](https://github.com/homarr-labs/homarr/commit/858fe6e19a682bb0fa0cfa0d11c2423447fdad2f))
|
||||
* **deps:** update tanstack-query monorepo to ^5.71.5 ([#2780](https://github.com/homarr-labs/homarr/issues/2780)) ([466d0e0](https://github.com/homarr-labs/homarr/commit/466d0e00530903ab4101cd2e94c00ab8a7c1c8ce))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.24.0 ([#2789](https://github.com/homarr-labs/homarr/issues/2789)) ([1b05041](https://github.com/homarr-labs/homarr/commit/1b0504198b41032319ead2604721abaaf08709d8))
|
||||
* **deps:** update tiptap monorepo to v2.11.7 ([#2749](https://github.com/homarr-labs/homarr/issues/2749)) ([0ec8f23](https://github.com/homarr-labs/homarr/commit/0ec8f23b7c655889d8f6fb9fcfd66a9736eb5a10))
|
||||
* **deps:** update tiptap monorepo to v2.11.7 ([#2750](https://github.com/homarr-labs/homarr/issues/2750)) ([59b1630](https://github.com/homarr-labs/homarr/commit/59b16305326ac811ad91e89c0786b9e6b1b7e1df))
|
||||
* **deps:** update trpc monorepo to ^11.0.2 ([#2792](https://github.com/homarr-labs/homarr/issues/2792)) ([8886380](https://github.com/homarr-labs/homarr/commit/8886380f0a9ea7d35cd54182a378b2f47b71c283))
|
||||
* **deps:** update turbo monorepo to ^2.5.0 ([#2791](https://github.com/homarr-labs/homarr/issues/2791)) ([0b41967](https://github.com/homarr-labs/homarr/commit/0b41967ec366d925dbb172d38c3677d88f51f0a9))
|
||||
* **nextcloud:** issue with self signed certificates ([#2760](https://github.com/homarr-labs/homarr/issues/2760)) ([7f77130](https://github.com/homarr-labs/homarr/commit/7f77130978eb7f25e571b7315250c91656a85d78))
|
||||
* update check blocks page loading ([#2782](https://github.com/homarr-labs/homarr/issues/2782)) ([29ff879](https://github.com/homarr-labs/homarr/commit/29ff879dfd1036d44b7c4735a0b269a83173b4d1))
|
||||
|
||||
### Reverts
|
||||
|
||||
* add restriction callback to restrict visibility and modification of widget kinds ([#2746](https://github.com/homarr-labs/homarr/issues/2746)) ([9851531](https://github.com/homarr-labs/homarr/commit/98515312a2439d1ed9fc5b412843ca625120b2ac))
|
||||
|
||||
## [1.13.1](https://github.com/homarr-labs/homarr/compare/v1.13.0...v1.13.1) (2025-03-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update react monorepo to v19.1.0 ([#2734](https://github.com/homarr-labs/homarr/issues/2734)) ([94ff401](https://github.com/homarr-labs/homarr/commit/94ff401d7dface924f48affccbc59e3a98ed8dc1))
|
||||
|
||||
### Reverts
|
||||
|
||||
* ssg integration create ([#2736](https://github.com/homarr-labs/homarr/issues/2736)) ([fef50a5](https://github.com/homarr-labs/homarr/commit/fef50a53b1fb592668dfa9aade6b110e42aec9a8))
|
||||
|
||||
## [1.13.0](https://github.com/homarr-labs/homarr/compare/v1.12.0...v1.13.0) (2025-03-28)
|
||||
|
||||
### Features
|
||||
|
||||
* add default search engines seeding / set homarr docs as global server search engine ([#2663](https://github.com/homarr-labs/homarr/issues/2663)) ([3e1c000](https://github.com/homarr-labs/homarr/commit/3e1c000d515c0edc6f49241fc381187e1829ca7e))
|
||||
* add german swiss locale ([#2726](https://github.com/homarr-labs/homarr/issues/2726)) ([eba738e](https://github.com/homarr-labs/homarr/commit/eba738e5d6594d46b5b7d1eab875fc8060e0fe7b))
|
||||
* add jetbrains run profile ([#2704](https://github.com/homarr-labs/homarr/issues/2704)) ([07f0abf](https://github.com/homarr-labs/homarr/commit/07f0abf28cb31f81860e623c70970e16d17e2836))
|
||||
* add title to dynamic section ([#2614](https://github.com/homarr-labs/homarr/issues/2614)) ([1dd7b1a](https://github.com/homarr-labs/homarr/commit/1dd7b1a6b97f75bca0d1a5c0dbb4254aa4bbd418))
|
||||
* app dropdown add infinity scroll ([#2696](https://github.com/homarr-labs/homarr/issues/2696)) ([8bf893b](https://github.com/homarr-labs/homarr/commit/8bf893b39292f34f5c697c09ba2c9e53c8a6ea90))
|
||||
* **auth:** add env variable to force user-info usage instead of idtoken ([#2711](https://github.com/homarr-labs/homarr/issues/2711)) ([94d3dad](https://github.com/homarr-labs/homarr/commit/94d3dad909c39cec096ff61c0a2f45e8853b6111))
|
||||
* auto sign-in when creating the initial user ([#2664](https://github.com/homarr-labs/homarr/issues/2664)) ([46c95e0](https://github.com/homarr-labs/homarr/commit/46c95e0da3ad27eb072efc45250abb0baf3e2483))
|
||||
* implement auto-select icon ([#2679](https://github.com/homarr-labs/homarr/issues/2679)) ([1a3a559](https://github.com/homarr-labs/homarr/commit/1a3a55934d8b3e4703e0ce44d9d26ad7ff100aca))
|
||||
* **lang:** add english uk language ([#2715](https://github.com/homarr-labs/homarr/issues/2715)) ([93c64ba](https://github.com/homarr-labs/homarr/commit/93c64baec294c39cd490a85f8b06b57d53fc82b3))
|
||||
* **logs:** improve logs by logging errors with causes and metadata ([#2703](https://github.com/homarr-labs/homarr/issues/2703)) ([579dd57](https://github.com/homarr-labs/homarr/commit/579dd5763d139e1644c2e192620728afa54c96d2))
|
||||
* set home board when creating first board ([#2683](https://github.com/homarr-labs/homarr/issues/2683)) ([8f7f211](https://github.com/homarr-labs/homarr/commit/8f7f2110ec29db12ea2d83cd779ec86851209188))
|
||||
* SSG integration creation page ([#2684](https://github.com/homarr-labs/homarr/issues/2684)) ([ce0705e](https://github.com/homarr-labs/homarr/commit/ce0705e9d4a502273a0d84e4e14d61a061fd21f9))
|
||||
* **widget:** add restriction callback to restrict visibility and modification of widget kinds ([#2658](https://github.com/homarr-labs/homarr/issues/2658)) ([84f73d3](https://github.com/homarr-labs/homarr/commit/84f73d33a061a1d243803078b245a8016a615af9))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow deletion of external users ([#2725](https://github.com/homarr-labs/homarr/issues/2725)) ([40e0098](https://github.com/homarr-labs/homarr/commit/40e00987cb6f4ee58fdce1da9489eedd92b8843d))
|
||||
* **auth:** fusion auth returns www-authenticate token response ([#2719](https://github.com/homarr-labs/homarr/issues/2719)) ([b5b0b06](https://github.com/homarr-labs/homarr/commit/b5b0b069d2029110d5618a29af5ae003030584d9))
|
||||
* bookmarks layout ([#2675](https://github.com/homarr-labs/homarr/issues/2675)) ([8a071f9](https://github.com/homarr-labs/homarr/commit/8a071f9ab6bc4c85073dc52cc2e60da69f30950a))
|
||||
* calendar indicators ([#2712](https://github.com/homarr-labs/homarr/issues/2712)) ([70ea093](https://github.com/homarr-labs/homarr/commit/70ea093eb6530d25d3e60e6d496651e3fc457826))
|
||||
* **deps:** update dependency drizzle-kit to ^0.30.6 ([#2706](https://github.com/homarr-labs/homarr/issues/2706)) ([f03e381](https://github.com/homarr-labs/homarr/commit/f03e381aea414c5fb770c7ba092b31f9c53eee11))
|
||||
* **deps:** update dependency drizzle-zod to ^0.7.1 ([#2717](https://github.com/homarr-labs/homarr/issues/2717)) ([569ec6f](https://github.com/homarr-labs/homarr/commit/569ec6fe5ccec980351162024170fabe06dcc3da))
|
||||
* **deps:** update dependency ldapts to v7.3.3 ([#2691](https://github.com/homarr-labs/homarr/issues/2691)) ([00f5a87](https://github.com/homarr-labs/homarr/commit/00f5a877a6f09037eff4c9934fc273c4d2af0f6c))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.20.2 ([#2708](https://github.com/homarr-labs/homarr/issues/2708)) ([68407ea](https://github.com/homarr-labs/homarr/commit/68407eaa195894db37018cefeeca4ae2da085847))
|
||||
* **deps:** update dependency typescript-eslint to ^8.28.0 ([#2693](https://github.com/homarr-labs/homarr/issues/2693)) ([f624611](https://github.com/homarr-labs/homarr/commit/f6246115405cdc85948059170742b0bae58ab337))
|
||||
* **deps:** update dependency undici to v7.6.0 ([#2710](https://github.com/homarr-labs/homarr/issues/2710)) ([43b8cfa](https://github.com/homarr-labs/homarr/commit/43b8cfa4dd250013fdf15d509a072ba0984f9ab1))
|
||||
* **deps:** update mantine monorepo to ^7.17.3 ([#2707](https://github.com/homarr-labs/homarr/issues/2707)) ([6854e47](https://github.com/homarr-labs/homarr/commit/6854e4729d3ba4a82a318813fec0a20eccdbed18))
|
||||
* **deps:** update nextjs monorepo to v15.2.4 ([#2720](https://github.com/homarr-labs/homarr/issues/2720)) ([ac77bdd](https://github.com/homarr-labs/homarr/commit/ac77bdd84cba47a43d1952725a9c7feccd972766))
|
||||
* **deps:** update tanstack-query monorepo to ^5.69.2 ([#2714](https://github.com/homarr-labs/homarr/issues/2714)) ([78b5520](https://github.com/homarr-labs/homarr/commit/78b55202e7f4739933768d27211556f927b04fed))
|
||||
* **deps:** update tanstack-query monorepo to ^5.69.3 ([#2716](https://github.com/homarr-labs/homarr/issues/2716)) ([e7e80db](https://github.com/homarr-labs/homarr/commit/e7e80dbc7d62830ce1ce150b6e22a67220da8be8))
|
||||
* **deps:** update tanstack-query monorepo to ^5.70.0 ([#2722](https://github.com/homarr-labs/homarr/issues/2722)) ([34eb9b3](https://github.com/homarr-labs/homarr/commit/34eb9b366fdc58c9fc03d18ac7dd68bff1ea83e6))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.22.0 ([#2692](https://github.com/homarr-labs/homarr/issues/2692)) ([b7a6d95](https://github.com/homarr-labs/homarr/commit/b7a6d950db557ea9deadc0be014b5f218611ac83))
|
||||
* **deps:** update testcontainers-node monorepo to ^10.23.0 ([#2699](https://github.com/homarr-labs/homarr/issues/2699)) ([c3264b2](https://github.com/homarr-labs/homarr/commit/c3264b2fe10ad374e92eb16921d43427d7ca866b))
|
||||
* **deps:** update tiptap monorepo to v2.11.6 ([#2718](https://github.com/homarr-labs/homarr/issues/2718)) ([0ed4c74](https://github.com/homarr-labs/homarr/commit/0ed4c741c59c3e498aa247adc292bdbbde1c59d3))
|
||||
* **deps:** update trpc monorepo to ^11.0.1 ([#2724](https://github.com/homarr-labs/homarr/issues/2724)) ([2178185](https://github.com/homarr-labs/homarr/commit/217818573e8bd7d49a821afdf269cd8b3f3d442d))
|
||||
* **import:** autofix missing shapes for sidebars and some sections as well ([#2723](https://github.com/homarr-labs/homarr/issues/2723)) ([62c7955](https://github.com/homarr-labs/homarr/commit/62c7955e48ca62b4bf1baf26ac6a12847795ac18))
|
||||
* Media request via search engine ([#2677](https://github.com/homarr-labs/homarr/issues/2677)) ([5a8405a](https://github.com/homarr-labs/homarr/commit/5a8405a9a81257b023c0265309f7f530b2e23c7a))
|
||||
|
||||
### Reverts
|
||||
|
||||
* add development package and update workspace ([#2700](https://github.com/homarr-labs/homarr/issues/2700))" ([#2728](https://github.com/homarr-labs/homarr/issues/2728)) ([996ec54](https://github.com/homarr-labs/homarr/commit/996ec548c4b9c8561919409b81adc90b0a24fbd6))
|
||||
|
||||
## [1.12.0](https://github.com/homarr-labs/homarr/compare/v1.11.0...v1.12.0) (2025-03-21)
|
||||
|
||||
### Features
|
||||
|
||||
+24
-24
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "pnpm with-env next build",
|
||||
"clean": "git clean -xdf .next .turbo node_modules",
|
||||
"dev": "pnpm with-env next dev --turbopack",
|
||||
"dev": "pnpm with-env next dev",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"start": "pnpm with-env next start",
|
||||
@@ -48,21 +48,21 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@mantine/colors-generator": "^7.17.3",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@mantine/dropzone": "^7.17.3",
|
||||
"@mantine/hooks": "^7.17.3",
|
||||
"@mantine/modals": "^7.17.3",
|
||||
"@mantine/tiptap": "^7.17.3",
|
||||
"@mantine/colors-generator": "^7.17.4",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/dropzone": "^7.17.4",
|
||||
"@mantine/hooks": "^7.17.4",
|
||||
"@mantine/modals": "^7.17.4",
|
||||
"@mantine/tiptap": "^7.17.4",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tanstack/react-query": "^5.70.0",
|
||||
"@tanstack/react-query-devtools": "^5.70.0",
|
||||
"@tanstack/react-query-next-experimental": "^5.70.0",
|
||||
"@trpc/client": "^11.0.1",
|
||||
"@trpc/next": "^11.0.1",
|
||||
"@trpc/react-query": "^11.0.1",
|
||||
"@trpc/server": "^11.0.1",
|
||||
"@tanstack/react-query": "^5.72.1",
|
||||
"@tanstack/react-query-devtools": "^5.72.1",
|
||||
"@tanstack/react-query-next-experimental": "^5.72.1",
|
||||
"@trpc/client": "^11.0.4",
|
||||
"@trpc/next": "^11.0.4",
|
||||
"@trpc/react-query": "^11.0.4",
|
||||
"@trpc/server": "^11.0.4",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
@@ -74,16 +74,16 @@
|
||||
"glob": "^11.0.1",
|
||||
"jotai": "^2.12.2",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.2.4",
|
||||
"next": "15.2.5",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.86.0",
|
||||
"sass": "^1.86.3",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.20.2",
|
||||
"swagger-ui-react": "^5.20.7",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
@@ -92,15 +92,15 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.0.12",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint": "^9.24.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { CustomPasswordInput } from "@homarr/ui";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userRegistrationSchema } from "@homarr/validation/user";
|
||||
|
||||
interface RegistrationFormProps {
|
||||
invite: {
|
||||
@@ -22,7 +22,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
|
||||
const t = useScopedI18n("user");
|
||||
const router = useRouter();
|
||||
const { mutate, isPending } = clientApi.user.register.useMutation();
|
||||
const form = useZodForm(validation.user.registration, {
|
||||
const form = useZodForm(userRegistrationSchema, {
|
||||
initialValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
@@ -30,7 +30,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (values: z.infer<typeof validation.user.registration>) => {
|
||||
const handleSubmit = (values: z.infer<typeof userRegistrationSchema>) => {
|
||||
mutate(
|
||||
{
|
||||
...values,
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { useForm } from "@homarr/form";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userSignInSchema } from "@homarr/validation/user";
|
||||
|
||||
interface LoginFormProps {
|
||||
providers: string[];
|
||||
@@ -22,7 +22,7 @@ interface LoginFormProps {
|
||||
callbackUrl: string;
|
||||
}
|
||||
|
||||
const extendedValidation = validation.user.signIn.extend({ provider: z.enum(["credentials", "ldap"]) });
|
||||
const extendedValidation = userSignInSchema.extend({ provider: z.enum(["credentials", "ldap"]) });
|
||||
|
||||
export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, callbackUrl }: LoginFormProps) => {
|
||||
const t = useScopedI18n("user");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import type { MantineColorsTuple } from "@mantine/core";
|
||||
import { colorsTuple, createTheme, darken, lighten, MantineProvider } from "@mantine/core";
|
||||
import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem } from "@mantine/core";
|
||||
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import type { ColorScheme } from "@homarr/definitions";
|
||||
@@ -24,6 +24,11 @@ export const BoardMantineProvider = ({
|
||||
},
|
||||
primaryColor: "primaryColor",
|
||||
autoContrast: true,
|
||||
fontSizes: {
|
||||
"2xl": rem(24),
|
||||
"3xl": rem(28),
|
||||
"4xl": rem(36),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
|
||||
|
||||
import type { Board } from "../../_types";
|
||||
import { generateColors } from "../../(content)/_theme";
|
||||
@@ -35,7 +35,7 @@ const hexRegex = /^#[0-9a-fA-F]{6}$/;
|
||||
const progressPercentageLabel = (value: number) => `${value}%`;
|
||||
|
||||
export const ColorSettingsContent = ({ board }: Props) => {
|
||||
const form = useZodForm(validation.board.savePartialSettings, {
|
||||
const form = useZodForm(boardSavePartialSettingsSchema, {
|
||||
initialValues: {
|
||||
primaryColor: board.primaryColor,
|
||||
secondaryColor: board.secondaryColor,
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Grid, Group, Stack, TextInput } from "@mantine/core";
|
||||
import { Autocomplete, Button, Center, Grid, Group, Popover, Stack, Text } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconPhotoOff } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { SelectItemWithDescriptionBadge } from "@homarr/ui";
|
||||
import { SelectWithDescriptionBadge } from "@homarr/ui";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
|
||||
|
||||
import type { Board } from "../../_types";
|
||||
import { useSavePartialSettingsMutation } from "./_shared";
|
||||
@@ -18,8 +22,9 @@ interface Props {
|
||||
}
|
||||
export const BackgroundSettingsContent = ({ board }: Props) => {
|
||||
const t = useI18n();
|
||||
const { data: session } = useSession();
|
||||
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
||||
const form = useZodForm(validation.board.savePartialSettings, {
|
||||
const form = useZodForm(boardSavePartialSettingsSchema, {
|
||||
initialValues: {
|
||||
backgroundImageUrl: board.backgroundImageUrl ?? "",
|
||||
backgroundImageAttachment: board.backgroundImageAttachment,
|
||||
@@ -28,6 +33,16 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
||||
},
|
||||
});
|
||||
|
||||
const [debouncedSearch] = useDebouncedValue(form.values.backgroundImageUrl, 200);
|
||||
const medias = clientApi.media.getPaginated.useQuery({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
includeFromAllUsers: true,
|
||||
search: debouncedSearch ?? "",
|
||||
});
|
||||
const images = medias.data?.items.filter((media) => media.contentType.startsWith("image/")) ?? [];
|
||||
const imageMap = new Map(images.map((image) => [`/api/user-medias/${image.id}`, image]));
|
||||
|
||||
const backgroundImageAttachmentData = useBackgroundOptionData(
|
||||
"backgroundImageAttachment",
|
||||
backgroundImageAttachments,
|
||||
@@ -47,8 +62,56 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
||||
<Stack>
|
||||
<Grid>
|
||||
<Grid.Col span={12}>
|
||||
<TextInput
|
||||
<Autocomplete
|
||||
leftSection={
|
||||
form.values.backgroundImageUrl &&
|
||||
form.values.backgroundImageUrl.trim().length >= 2 && (
|
||||
<Popover width={300} withArrow>
|
||||
<Popover.Target>
|
||||
<Center h="100%">
|
||||
<ImagePreview src={form.values.backgroundImageUrl} w={20} h={20} />
|
||||
</Center>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<ImagePreview src={form.values.backgroundImageUrl} w="100%" />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
// We filter it on the server
|
||||
filter={({ options }) => options}
|
||||
label={t("board.field.backgroundImageUrl.label")}
|
||||
placeholder={`${t("board.field.backgroundImageUrl.placeholder")}...`}
|
||||
renderOption={({ option }) => {
|
||||
const current = imageMap.get(option.value);
|
||||
if (!current) return null;
|
||||
|
||||
return (
|
||||
<Group gap="sm">
|
||||
<ImagePreview src={option.value} w={20} h={20} />
|
||||
<Stack gap={0}>
|
||||
<Text size="sm">{current.name}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{option.value}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}}
|
||||
data={[
|
||||
{
|
||||
group: t("board.field.backgroundImageUrl.group.your"),
|
||||
items: images
|
||||
.filter((media) => media.creatorId === session?.user.id)
|
||||
.map((media) => `/api/user-medias/${media.id}`),
|
||||
},
|
||||
{
|
||||
group: t("board.field.backgroundImageUrl.group.other"),
|
||||
items: images
|
||||
.filter((media) => media.creatorId !== session?.user.id)
|
||||
.map((media) => `/api/user-medias/${media.id}`),
|
||||
},
|
||||
]}
|
||||
{...form.getInputProps("backgroundImageUrl")}
|
||||
/>
|
||||
</Grid.Col>
|
||||
@@ -85,6 +148,21 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface ImagePreviewProps {
|
||||
src: string;
|
||||
w: string | number;
|
||||
h?: string | number;
|
||||
}
|
||||
|
||||
const ImagePreview = ({ src, w, h }: ImagePreviewProps) => {
|
||||
if (!["/", "http://", "https://"].some((prefix) => src.startsWith(prefix))) {
|
||||
return <IconPhotoOff size={w} />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img src={src} alt="preview image" style={{ width: w, height: h, objectFit: "contain" }} />;
|
||||
};
|
||||
|
||||
type BackgroundImageKey = "backgroundImageAttachment" | "backgroundImageSize" | "backgroundImageRepeat";
|
||||
|
||||
type inferOptions<TKey extends BackgroundImageKey> = TranslationObject["board"]["field"][TKey]["option"];
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Button, Grid, Group, Loader, Stack, TextInput, Tooltip } from "@mantine/core";
|
||||
import { Button, Grid, Group, Loader, Stack, TextInput } from "@mantine/core";
|
||||
import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
|
||||
import { useUpdateBoard } from "@homarr/boards/updater";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { IconPicker } from "@homarr/forms-collection";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
|
||||
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import type { Board } from "../../_types";
|
||||
@@ -28,7 +28,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
||||
|
||||
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
||||
const form = useZodForm(
|
||||
validation.board.savePartialSettings
|
||||
boardSavePartialSettingsSchema
|
||||
.pick({
|
||||
pageTitle: true,
|
||||
logoImageUrl: true,
|
||||
@@ -52,9 +52,9 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
useLogoPreview(form.values.logoImageUrl);
|
||||
useFaviconPreview(form.values.faviconImageUrl);
|
||||
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle);
|
||||
const faviconStatus = useFaviconPreview(form.values.faviconImageUrl);
|
||||
const logoStatus = useLogoPreview(form.values.logoImageUrl);
|
||||
|
||||
// Cleanup for not applied changes of the page title and logo image URL
|
||||
useEffect(() => {
|
||||
@@ -94,24 +94,24 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
||||
<TextInput
|
||||
label={t("board.field.metaTitle.label")}
|
||||
placeholder={createMetaTitle(t("board.content.metaTitle", { boardName: board.name }))}
|
||||
rightSection={<PendingOrInvalidIndicator {...metaTitleStatus} />}
|
||||
rightSection={metaTitleStatus.isPending && <Loader size="xs" />}
|
||||
{...form.getInputProps("metaTitle")}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ xs: 12, md: 6 }}>
|
||||
<TextInput
|
||||
<IconPicker
|
||||
{...form.getInputProps("logoImageUrl")}
|
||||
label={t("board.field.logoImageUrl.label")}
|
||||
placeholder="/logo/logo.png"
|
||||
rightSection={<PendingOrInvalidIndicator {...logoStatus} />}
|
||||
{...form.getInputProps("logoImageUrl")}
|
||||
withAsterisk={false}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ xs: 12, md: 6 }}>
|
||||
<TextInput
|
||||
<IconPicker
|
||||
{...form.getInputProps("faviconImageUrl")}
|
||||
label={t("board.field.faviconImageUrl.label")}
|
||||
placeholder="/logo/logo.png"
|
||||
rightSection={<PendingOrInvalidIndicator {...faviconStatus} />}
|
||||
{...form.getInputProps("faviconImageUrl")}
|
||||
withAsterisk={false}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
@@ -125,40 +125,16 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PendingOrInvalidIndicator = ({ isPending, isInvalid }: { isPending: boolean; isInvalid?: boolean }) => {
|
||||
const t = useI18n();
|
||||
|
||||
if (isInvalid) {
|
||||
return (
|
||||
<Tooltip multiline w={220} label={t("board.setting.section.general.unrecognizedLink")}>
|
||||
<IconAlertTriangle size="1rem" color="red" />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
return <Loader size="xs" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const useLogoPreview = (url: string | null) => {
|
||||
const { updateBoard } = useUpdateBoard();
|
||||
const [logoDebounced] = useDebouncedValue(url ?? "", 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logoDebounced.includes(".") && logoDebounced.length >= 1) return;
|
||||
updateBoard((previous) => ({
|
||||
...previous,
|
||||
logoImageUrl: logoDebounced.length >= 1 ? logoDebounced : null,
|
||||
}));
|
||||
}, [logoDebounced, updateBoard]);
|
||||
|
||||
return {
|
||||
isPending: (url ?? "") !== logoDebounced,
|
||||
isInvalid: logoDebounced.length >= 1 && !logoDebounced.includes("."),
|
||||
};
|
||||
};
|
||||
|
||||
const useMetaTitlePreview = (title: string | null) => {
|
||||
@@ -170,16 +146,7 @@ const useMetaTitlePreview = (title: string | null) => {
|
||||
};
|
||||
};
|
||||
|
||||
const validFaviconExtensions = ["ico", "png", "svg", "gif"];
|
||||
const isValidUrl = (url: string) =>
|
||||
url.includes("/") && validFaviconExtensions.some((extension) => url.endsWith(`.${extension}`));
|
||||
|
||||
const useFaviconPreview = (url: string | null) => {
|
||||
const [faviconDebounced] = useDebouncedValue(url ?? "", 500);
|
||||
useFavicon(isValidUrl(faviconDebounced) ? faviconDebounced : "");
|
||||
|
||||
return {
|
||||
isPending: (url ?? "") !== faviconDebounced,
|
||||
isInvalid: faviconDebounced.length >= 1 && !isValidUrl(faviconDebounced),
|
||||
};
|
||||
useFavicon(faviconDebounced);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { createId } from "@homarr/db/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { boardSaveLayoutsSchema } from "@homarr/validation/board";
|
||||
|
||||
import type { Board } from "../../_types";
|
||||
|
||||
@@ -22,7 +22,7 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
||||
void utils.board.getHomeBoard.invalidate();
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.board.saveLayouts.omit({ id: true }).required(), {
|
||||
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
|
||||
initialValues: {
|
||||
layouts: board.layouts,
|
||||
},
|
||||
|
||||
@@ -8,18 +8,18 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { groupCreateSchema } from "@homarr/validation/group";
|
||||
|
||||
export const InitGroup = () => {
|
||||
const t = useI18n();
|
||||
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
|
||||
const form = useZodForm(validation.group.create, {
|
||||
const form = useZodForm(groupCreateSchema, {
|
||||
initialValues: {
|
||||
name: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmitAsync = async (values: z.infer<typeof validation.group.create>) => {
|
||||
const handleSubmitAsync = async (values: z.infer<typeof groupCreateSchema>) => {
|
||||
await mutateAsync(values, {
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
|
||||
@@ -12,13 +12,13 @@ import type { CheckboxProps } from "@homarr/form/types";
|
||||
import { defaultServerSettings } from "@homarr/server-settings";
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { settingsInitSchema } from "@homarr/validation/settings";
|
||||
|
||||
export const InitSettings = () => {
|
||||
const tSection = useScopedI18n("management.page.settings.section");
|
||||
const t = useI18n();
|
||||
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation();
|
||||
const form = useZodForm(validation.settings.init, { initialValues: defaultServerSettings });
|
||||
const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings });
|
||||
|
||||
form.watch("analytics.enableGeneral", ({ value }) => {
|
||||
if (!value) {
|
||||
@@ -30,7 +30,7 @@ export const InitSettings = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmitAsync = async (values: z.infer<typeof validation.settings.init>) => {
|
||||
const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
|
||||
await mutateAsync(values, {
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
|
||||
@@ -10,13 +10,13 @@ import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { CustomPasswordInput } from "@homarr/ui";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userInitSchema } from "@homarr/validation/user";
|
||||
|
||||
export const InitUserForm = () => {
|
||||
const t = useScopedI18n("user");
|
||||
const tUser = useScopedI18n("init.step.user");
|
||||
const { mutateAsync, isPending } = clientApi.user.initUser.useMutation();
|
||||
const form = useZodForm(validation.user.init, {
|
||||
const form = useZodForm(userInitSchema, {
|
||||
initialValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
@@ -74,4 +74,4 @@ export const InitUserForm = () => {
|
||||
);
|
||||
};
|
||||
|
||||
type FormType = z.infer<typeof validation.user.init>;
|
||||
type FormType = z.infer<typeof userInitSchema>;
|
||||
@@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { AppForm } from "@homarr/forms-collection";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import type { validation } from "@homarr/validation";
|
||||
import type { appManageSchema } from "@homarr/validation/app";
|
||||
|
||||
interface AppEditFormProps {
|
||||
app: RouterOutputs["app"]["byId"];
|
||||
@@ -40,7 +40,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(values: z.infer<typeof validation.app.manage>) => {
|
||||
(values: z.infer<typeof appManageSchema>) => {
|
||||
mutate({
|
||||
id: app.id,
|
||||
...values,
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@ import { convertIntegrationTestConnectionError } from "@homarr/integrations/clie
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { integrationUpdateSchema } from "@homarr/validation/integration";
|
||||
|
||||
import { SecretCard } from "../../_components/secrets/integration-secret-card";
|
||||
import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs";
|
||||
@@ -32,7 +32,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
) ?? getDefaultSecretKinds(integration.kind);
|
||||
|
||||
const router = useRouter();
|
||||
const form = useZodForm(validation.integration.update.omit({ id: true }), {
|
||||
const form = useZodForm(integrationUpdateSchema.omit({ id: true }), {
|
||||
initialValues: {
|
||||
name: integration.name,
|
||||
url: integration.url,
|
||||
@@ -141,4 +141,4 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
);
|
||||
};
|
||||
|
||||
type FormType = Omit<z.infer<typeof validation.integration.update>, "id">;
|
||||
type FormType = Omit<z.infer<typeof integrationUpdateSchema>, "id">;
|
||||
@@ -39,7 +39,7 @@ export const IntegrationCreateDropdownContent = () => {
|
||||
{filteredKinds.length > 0 ? (
|
||||
<ScrollArea.Autosize mah={384}>
|
||||
{filteredKinds.map((kind) => (
|
||||
<Menu.Item component={Link} href={`/manage/integrations/new/${kind}`} key={kind}>
|
||||
<Menu.Item component={Link} href={`/manage/integrations/new?kind=${kind}`} key={kind}>
|
||||
<Group>
|
||||
<IntegrationAvatar kind={kind} size="sm" />
|
||||
<Text size="sm">{getIntegrationName(kind)}</Text>
|
||||
|
||||
@@ -27,20 +27,21 @@ import { useZodForm } from "@homarr/form";
|
||||
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { appHrefSchema } from "@homarr/validation/app";
|
||||
import { integrationCreateSchema } from "@homarr/validation/integration";
|
||||
|
||||
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
|
||||
|
||||
interface NewIntegrationFormProps {
|
||||
searchParams: Partial<z.infer<typeof validation.integration.create>> & {
|
||||
searchParams: Partial<z.infer<typeof integrationCreateSchema>> & {
|
||||
kind: IntegrationKind;
|
||||
};
|
||||
}
|
||||
|
||||
const formSchema = validation.integration.create.omit({ kind: true }).and(
|
||||
const formSchema = integrationCreateSchema.omit({ kind: true }).and(
|
||||
z.object({
|
||||
createApp: z.boolean(),
|
||||
appHref: validation.app.manage.shape.href,
|
||||
appHref: appHrefSchema,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+15
-21
@@ -3,55 +3,49 @@ import { Container, Group, Stack, Title } from "@mantine/core";
|
||||
import { z } from "zod";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { IntegrationAvatar } from "@homarr/ui";
|
||||
import type { validation } from "@homarr/validation";
|
||||
import type { integrationCreateSchema } from "@homarr/validation/integration";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { NewIntegrationForm } from "../_integration-new-form";
|
||||
import { NewIntegrationForm } from "./_integration-new-form";
|
||||
|
||||
interface NewIntegrationByIdPageProps {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
searchParams: Partial<z.infer<typeof validation.integration.create>>;
|
||||
interface NewIntegrationPageProps {
|
||||
searchParams: Promise<
|
||||
Partial<z.infer<typeof integrationCreateSchema>> & {
|
||||
kind: IntegrationKind;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return integrationKinds.map((kind) => ({
|
||||
id: kind,
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function IntegrationNewByIdPage(props: NewIntegrationByIdPageProps) {
|
||||
const { id } = props.params;
|
||||
export default async function IntegrationsNewPage(props: NewIntegrationPageProps) {
|
||||
const searchParams = await props.searchParams;
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user.permissions.includes("integration-create")) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const result = z.enum(integrationKinds).safeParse(id);
|
||||
const result = z.enum(integrationKinds).safeParse(searchParams.kind);
|
||||
if (!result.success) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const tCreate = await getScopedI18n("integration.page.create");
|
||||
const currentKind = result.data;
|
||||
|
||||
const dynamicMappings = new Map<string, string>([[id, getIntegrationName(currentKind)]]);
|
||||
const currentKind = result.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb dynamicMappings={dynamicMappings} nonInteractable={["new"]} />
|
||||
<DynamicBreadcrumb />
|
||||
<Container>
|
||||
<Stack>
|
||||
<Group align="center">
|
||||
<IntegrationAvatar kind={currentKind} size="md" />
|
||||
<Title>{tCreate("title", { name: getIntegrationName(currentKind) })}</Title>
|
||||
</Group>
|
||||
<NewIntegrationForm searchParams={{ kind: currentKind }} />
|
||||
<NewIntegrationForm searchParams={searchParams} />
|
||||
</Stack>
|
||||
</Container>
|
||||
</>
|
||||
@@ -12,9 +12,9 @@ import { useZodForm } from "@homarr/form";
|
||||
import { IconPicker } from "@homarr/forms-collection";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||
|
||||
type FormType = z.infer<typeof validation.searchEngine.manage>;
|
||||
type FormType = z.infer<typeof searchEngineManageSchema>;
|
||||
|
||||
interface SearchEngineFormProps {
|
||||
submitButtonTranslation: (t: TranslationFunction) => string;
|
||||
@@ -30,7 +30,7 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
|
||||
|
||||
const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery();
|
||||
|
||||
const form = useZodForm(validation.searchEngine.manage, {
|
||||
const form = useZodForm(searchEngineManageSchema, {
|
||||
initialValues: initialValues ?? {
|
||||
name: "",
|
||||
short: "",
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { validation } from "@homarr/validation";
|
||||
import type { searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||
|
||||
import { SearchEngineForm } from "../../_form";
|
||||
|
||||
@@ -41,7 +41,7 @@ export const SearchEngineEditForm = ({ searchEngine }: SearchEngineEditFormProps
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(values: z.infer<typeof validation.searchEngine.manage>) => {
|
||||
(values: z.infer<typeof searchEngineManageSchema>) => {
|
||||
mutate({
|
||||
id: searchEngine.id,
|
||||
...values,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { validation } from "@homarr/validation";
|
||||
import type { searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||
|
||||
import { SearchEngineForm } from "../_form";
|
||||
|
||||
@@ -35,7 +35,7 @@ export const SearchEngineNewForm = () => {
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(values: z.infer<typeof validation.searchEngine.manage>) => {
|
||||
(values: z.infer<typeof searchEngineManageSchema>) => {
|
||||
mutate(values);
|
||||
},
|
||||
[mutate],
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { superRefineCertificateFile } from "@homarr/validation";
|
||||
import { superRefineCertificateFile } from "@homarr/validation/certificates";
|
||||
|
||||
export const AddCertificateButton = () => {
|
||||
const { openModal } = useModalAction(AddCertificateModal);
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userChangeHomeBoardsSchema } from "@homarr/validation/user";
|
||||
|
||||
import type { Board } from "~/app/[locale]/boards/_types";
|
||||
import { BoardSelect } from "~/components/board/board-select";
|
||||
@@ -40,7 +40,7 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
|
||||
});
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.user.changeHomeBoards, {
|
||||
const form = useZodForm(userChangeHomeBoardsSchema, {
|
||||
initialValues: {
|
||||
homeBoardId: user.homeBoardId,
|
||||
mobileHomeBoardId: user.mobileHomeBoardId,
|
||||
@@ -82,4 +82,4 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
|
||||
);
|
||||
};
|
||||
|
||||
type FormType = z.infer<typeof validation.user.changeHomeBoards>;
|
||||
type FormType = z.infer<typeof userChangeHomeBoardsSchema>;
|
||||
+3
-3
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
|
||||
|
||||
interface ChangeSearchPreferencesFormProps {
|
||||
user: RouterOutputs["user"]["getById"];
|
||||
@@ -37,7 +37,7 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
|
||||
});
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.user.changeSearchPreferences, {
|
||||
const form = useZodForm(userChangeSearchPreferencesSchema, {
|
||||
initialValues: {
|
||||
defaultSearchEngineId: user.defaultSearchEngineId,
|
||||
openInNewTab: user.openSearchInNewTab,
|
||||
@@ -75,4 +75,4 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
|
||||
);
|
||||
};
|
||||
|
||||
type FormType = z.infer<typeof validation.user.changeSearchPreferences>;
|
||||
type FormType = z.infer<typeof userChangeSearchPreferencesSchema>;
|
||||
+3
-3
@@ -12,7 +12,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userFirstDayOfWeekSchema } from "@homarr/validation/user";
|
||||
|
||||
dayjs.extend(localeData);
|
||||
|
||||
@@ -42,7 +42,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
|
||||
});
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.user.firstDayOfWeek, {
|
||||
const form = useZodForm(userFirstDayOfWeekSchema, {
|
||||
initialValues: {
|
||||
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
|
||||
},
|
||||
@@ -80,4 +80,4 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
type FormType = z.infer<typeof validation.user.firstDayOfWeek>;
|
||||
type FormType = z.infer<typeof userFirstDayOfWeekSchema>;
|
||||
+3
-3
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userPingIconsEnabledSchema } from "@homarr/validation/user";
|
||||
|
||||
interface PingIconsEnabledProps {
|
||||
user: RouterOutputs["user"]["getById"];
|
||||
@@ -35,7 +35,7 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
|
||||
});
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.user.pingIconsEnabled, {
|
||||
const form = useZodForm(userPingIconsEnabledSchema, {
|
||||
initialValues: {
|
||||
pingIconsEnabled: user.pingIconsEnabled,
|
||||
},
|
||||
@@ -66,4 +66,4 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
type FormType = z.infer<typeof validation.user.pingIconsEnabled>;
|
||||
type FormType = z.infer<typeof userPingIconsEnabledSchema>;
|
||||
+2
-2
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userEditProfileSchema } from "@homarr/validation/user";
|
||||
|
||||
interface UserProfileFormProps {
|
||||
user: RouterOutputs["user"]["getById"];
|
||||
@@ -43,7 +43,7 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
|
||||
});
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.user.editProfile.omit({ id: true }), {
|
||||
const form = useZodForm(userEditProfileSchema.omit({ id: true }), {
|
||||
initialValues: {
|
||||
name: user.name ?? "",
|
||||
email: user.email ?? "",
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { CustomPasswordInput } from "@homarr/ui";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userChangePasswordSchema } from "@homarr/validation/user";
|
||||
|
||||
interface ChangePasswordFormProps {
|
||||
user: RouterOutputs["user"]["getById"];
|
||||
@@ -34,7 +34,7 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => {
|
||||
});
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.user.changePassword, {
|
||||
const form = useZodForm(userChangePasswordSchema, {
|
||||
initialValues: {
|
||||
/* Require previous password if the current user want's to change his password */
|
||||
previousPassword: session?.user.id === user.id ? "" : "_",
|
||||
|
||||
+3
-3
@@ -27,8 +27,8 @@ import { useModalAction } from "@homarr/modals";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { createCustomErrorParams } from "@homarr/validation/form";
|
||||
import { createCustomErrorParams } from "@homarr/validation/form/i18n";
|
||||
import { userPasswordSchema } from "@homarr/validation/user";
|
||||
|
||||
import { GroupSelectModal } from "~/components/access/group-select-modal";
|
||||
import { StepperNavigationComponent } from "./stepper-navigation";
|
||||
@@ -84,7 +84,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
|
||||
const securityForm = useZodForm(
|
||||
z
|
||||
.object({
|
||||
password: validation.user.password,
|
||||
password: userPasswordSchema,
|
||||
confirmPassword: z.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { groupUpdateSchema } from "@homarr/validation/group";
|
||||
|
||||
interface RenameGroupFormProps {
|
||||
group: {
|
||||
@@ -21,7 +21,7 @@ interface RenameGroupFormProps {
|
||||
export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
|
||||
const t = useI18n();
|
||||
const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
|
||||
const form = useZodForm(validation.group.update.pick({ name: true }), {
|
||||
const form = useZodForm(groupUpdateSchema.pick({ name: true }), {
|
||||
initialValues: {
|
||||
name: group.name,
|
||||
},
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { groupSettingsSchema } from "@homarr/validation/group";
|
||||
|
||||
import { BoardSelect } from "~/components/board/board-select";
|
||||
|
||||
@@ -19,7 +19,7 @@ interface GroupHomeBoardsProps {
|
||||
export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: GroupHomeBoardsProps) => {
|
||||
const t = useI18n();
|
||||
const [availableBoards] = clientApi.board.getBoardsForGroup.useSuspenseQuery({ groupId });
|
||||
const form = useZodForm(validation.group.settings.pick({ homeBoardId: true, mobileHomeBoardId: true }), {
|
||||
const form = useZodForm(groupSettingsSchema.pick({ homeBoardId: true, mobileHomeBoardId: true }), {
|
||||
initialValues: {
|
||||
homeBoardId,
|
||||
mobileHomeBoardId,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useModalAction } from "@homarr/modals";
|
||||
import { showSuccessNotification } from "@homarr/notifications";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
|
||||
import { WidgetError } from "@homarr/widgets/errors";
|
||||
import { WidgetEditModal } from "@homarr/widgets/modals";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useUpdateBoard } from "@homarr/boards/updater";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||
|
||||
import type { CreateItemInput } from "./actions/create-item";
|
||||
import { createItemCallback } from "./actions/create-item";
|
||||
|
||||
@@ -5,8 +5,6 @@ import combineClasses from "clsx";
|
||||
import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { isWidgetRestricted } from "@homarr/auth/shared";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
@@ -17,7 +15,6 @@ import type { SectionItem } from "~/app/[locale]/boards/_types";
|
||||
import classes from "../sections/item.module.css";
|
||||
import { useItemActions } from "./item-actions";
|
||||
import { BoardItemMenu } from "./item-menu";
|
||||
import { RestrictedWidgetContent } from "./restricted";
|
||||
|
||||
interface BoardItemContentProps {
|
||||
item: SectionItem;
|
||||
@@ -62,7 +59,6 @@ interface InnerContentProps {
|
||||
const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
|
||||
const settings = useSettings();
|
||||
const board = useRequiredBoard();
|
||||
const { data: session } = useSession();
|
||||
const [isEditMode] = useEditMode();
|
||||
const Comp = loadWidgetDynamic(item.kind);
|
||||
const { definition } = widgetImports[item.kind];
|
||||
@@ -74,16 +70,6 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
|
||||
const widgetSupportsIntegrations =
|
||||
"supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1;
|
||||
|
||||
if (
|
||||
isWidgetRestricted({
|
||||
definition,
|
||||
user: session?.user ?? null,
|
||||
check: (level) => level === "all",
|
||||
})
|
||||
) {
|
||||
return <RestrictedWidgetContent kind={item.kind} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryErrorResetBoundary>
|
||||
{({ reset }) => (
|
||||
|
||||
@@ -3,8 +3,6 @@ import { ActionIcon, Menu } from "@mantine/core";
|
||||
import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { isWidgetRestricted } from "@homarr/auth/shared";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
@@ -39,7 +37,6 @@ export const BoardItemMenu = ({
|
||||
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
|
||||
const { gridstack } = useSectionContext().refs;
|
||||
const settings = useSettings();
|
||||
const { data: session } = useSession();
|
||||
|
||||
// Reset error boundary on next render if item has been edited
|
||||
useEffect(() => {
|
||||
@@ -94,16 +91,6 @@ export const BoardItemMenu = ({
|
||||
});
|
||||
};
|
||||
|
||||
if (
|
||||
isWidgetRestricted({
|
||||
definition: currentDefinition,
|
||||
user: session?.user ?? null,
|
||||
check: (level) => level !== "none",
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu withinPortal withArrow position="right-start" arrowPosition="center">
|
||||
<Menu.Target>
|
||||
|
||||
@@ -2,8 +2,6 @@ import { useMemo, useState } from "react";
|
||||
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { isWidgetRestricted } from "@homarr/auth/shared";
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { createModal } from "@homarr/modals";
|
||||
@@ -17,18 +15,10 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
|
||||
const [search, setSearch] = useState("");
|
||||
const t = useI18n();
|
||||
const { createItem } = useItemActions();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
objectEntries(widgetImports)
|
||||
.filter(([, value]) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: value.definition,
|
||||
user: session?.user ?? null,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
})
|
||||
.map(([kind, value]) => ({
|
||||
kind,
|
||||
icon: value.definition.icon,
|
||||
@@ -36,7 +26,7 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
|
||||
description: t(`widget.${kind}.description`),
|
||||
}))
|
||||
.sort((itemA, itemB) => itemA.name.localeCompare(itemB.name)),
|
||||
[t, session?.user],
|
||||
[t],
|
||||
);
|
||||
|
||||
const filteredItems = useMemo(
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Center, Group, Stack, Text } from "@mantine/core";
|
||||
import { IconShield } from "@tabler/icons-react";
|
||||
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
interface RestrictedWidgetProps {
|
||||
kind: WidgetKind;
|
||||
}
|
||||
|
||||
export const RestrictedWidgetContent = ({ kind }: RestrictedWidgetProps) => {
|
||||
const tCurrentWidget = useScopedI18n(`widget.${kind}`);
|
||||
const tCommonWidget = useScopedI18n("widget.common");
|
||||
|
||||
return (
|
||||
<Center h="100%">
|
||||
<Stack ta="center" gap="xs" align="center">
|
||||
<Group gap="sm">
|
||||
<IconShield size={16} />
|
||||
<Text size="sm" fw="bold">
|
||||
{tCommonWidget("restricted.title")}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm">{tCommonWidget("restricted.description", { name: tCurrentWidget("name") })}</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { boardRenameSchema } from "@homarr/validation/board";
|
||||
|
||||
interface InnerProps {
|
||||
id: string;
|
||||
@@ -26,7 +26,7 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
|
||||
void utils.board.getHomeBoard.invalidate();
|
||||
},
|
||||
});
|
||||
const form = useZodForm(validation.board.rename.omit({ id: true }), {
|
||||
const form = useZodForm(boardRenameSchema.omit({ id: true }), {
|
||||
initialValues: {
|
||||
name: innerProps.previousName,
|
||||
},
|
||||
@@ -66,4 +66,4 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
|
||||
defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"),
|
||||
});
|
||||
|
||||
type FormType = Omit<z.infer<(typeof validation)["board"]["rename"]>, "id">;
|
||||
type FormType = Omit<z.infer<typeof boardRenameSchema>, "id">;
|
||||
@@ -2,7 +2,7 @@ import { useCallback } from "react";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { useUpdateBoard } from "@homarr/boards/updater";
|
||||
import type { dynamicSectionOptionsSchema } from "@homarr/validation";
|
||||
import type { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
||||
|
||||
import { addDynamicSectionCallback } from "./actions/add-dynamic-section";
|
||||
import type { RemoveDynamicSectionInput } from "./actions/remove-dynamic-section";
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { z } from "zod";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { dynamicSectionOptionsSchema } from "@homarr/validation";
|
||||
import { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
||||
|
||||
interface ModalProps {
|
||||
value: z.infer<typeof dynamicSectionOptionsSchema>;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { Suspense, use } from "react";
|
||||
import { Indicator, Menu, Text } from "@mantine/core";
|
||||
import { IconBellRinging } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
interface UpdateIndicatorProps extends PropsWithChildren {
|
||||
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]> | undefined;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const UpdateIndicator = ({ children, availableUpdatesPromise, disabled }: UpdateIndicatorProps) => {
|
||||
if (disabled || availableUpdatesPromise === undefined) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={children}>
|
||||
<InnerUpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={disabled}>
|
||||
{children}
|
||||
</InnerUpdateIndicator>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
interface InnerUpdateIndicatorProps extends PropsWithChildren {
|
||||
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const InnerUpdateIndicator = ({ children, disabled, availableUpdatesPromise }: InnerUpdateIndicatorProps) => {
|
||||
const availableUpdates = use(availableUpdatesPromise);
|
||||
|
||||
return (
|
||||
<Indicator
|
||||
disabled={!availableUpdates || availableUpdates.length === 0 || disabled}
|
||||
size={15}
|
||||
processing
|
||||
withBorder
|
||||
>
|
||||
{children}
|
||||
</Indicator>
|
||||
);
|
||||
};
|
||||
|
||||
interface AvailableUpdatesMenuItemProps {
|
||||
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]> | undefined;
|
||||
}
|
||||
|
||||
export const AvailableUpdatesMenuItem = ({ availableUpdatesPromise }: AvailableUpdatesMenuItemProps) => {
|
||||
if (availableUpdatesPromise === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<InnerAvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
interface InnerAvailableUpdatesMenuItemProps {
|
||||
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
|
||||
}
|
||||
|
||||
const InnerAvailableUpdatesMenuItem = ({ availableUpdatesPromise }: InnerAvailableUpdatesMenuItemProps) => {
|
||||
const t = useScopedI18n("common.userAvatar.menu");
|
||||
const availableUpdates = use(availableUpdatesPromise);
|
||||
if (availableUpdates === undefined || availableUpdates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const latestUpdate = availableUpdates.at(0);
|
||||
if (!latestUpdate) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Item component={"a"} href={latestUpdate.url} target="_blank" leftSection={<IconBellRinging size="1rem" />}>
|
||||
<Text fw="bold" size="sm">
|
||||
{t("updateAvailable", {
|
||||
countUpdates: String(availableUpdates.length),
|
||||
tag: latestUpdate.tagName,
|
||||
})}
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +1,25 @@
|
||||
import { Indicator, UnstyledButton } from "@mantine/core";
|
||||
import { Suspense } from "react";
|
||||
import { UnstyledButton } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
|
||||
import { CurrentUserAvatar } from "~/components/user-avatar";
|
||||
import { UserAvatarMenu } from "~/components/user-avatar-menu";
|
||||
import { UpdateIndicator } from "./update";
|
||||
|
||||
export const UserButton = async () => {
|
||||
const session = await auth();
|
||||
const isAdmin = session?.user.permissions.includes("admin");
|
||||
const data = isAdmin ? await api.updateChecker.getAvailableUpdates() : undefined;
|
||||
const availableUpdatesPromise = isAdmin ? api.updateChecker.getAvailableUpdates() : undefined;
|
||||
return (
|
||||
<UserAvatarMenu availableUpdates={data}>
|
||||
<UserAvatarMenu availableUpdatesPromise={availableUpdatesPromise}>
|
||||
<UnstyledButton>
|
||||
<Indicator disabled={data?.length === 0 || !isAdmin} size={15} processing withBorder>
|
||||
<CurrentUserAvatar size="md" />
|
||||
</Indicator>
|
||||
<Suspense fallback={<CurrentUserAvatar size="md" />}>
|
||||
<UpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={!isAdmin}>
|
||||
<CurrentUserAvatar size="md" />
|
||||
</UpdateIndicator>
|
||||
</Suspense>
|
||||
</UnstyledButton>
|
||||
</UserAvatarMenu>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useRouter } from "next/navigation";
|
||||
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
||||
import { useHotkeys, useTimeout } from "@mantine/hooks";
|
||||
import {
|
||||
IconBellRinging,
|
||||
IconCheck,
|
||||
IconHome,
|
||||
IconLogin,
|
||||
@@ -25,13 +24,14 @@ import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
|
||||
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
|
||||
import { AvailableUpdatesMenuItem } from "./layout/header/update";
|
||||
|
||||
interface UserAvatarMenuProps {
|
||||
children: ReactNode;
|
||||
availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"];
|
||||
availableUpdatesPromise?: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
|
||||
}
|
||||
|
||||
export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuProps) => {
|
||||
export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatarMenuProps) => {
|
||||
const t = useScopedI18n("common.userAvatar.menu");
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
useHotkeys([["mod+J", toggleColorScheme]]);
|
||||
@@ -65,24 +65,7 @@ export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuPro
|
||||
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
|
||||
<Menu width={300} withArrow withinPortal keepMounted>
|
||||
<Menu.Dropdown>
|
||||
{availableUpdates && availableUpdates.length > 0 && availableUpdates[0] && (
|
||||
<>
|
||||
<Menu.Item
|
||||
component={"a"}
|
||||
href={availableUpdates[0].url}
|
||||
target="_blank"
|
||||
leftSection={<IconBellRinging size="1rem" />}
|
||||
>
|
||||
<Text fw="bold" size="sm">
|
||||
{t("updateAvailable", {
|
||||
countUpdates: String(availableUpdates.length),
|
||||
tag: availableUpdates[0].tagName,
|
||||
})}
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
<AvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
|
||||
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
|
||||
{colorSchemeText}
|
||||
</Menu.Item>
|
||||
|
||||
@@ -38,17 +38,17 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.6.0"
|
||||
"undici": "7.7.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.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint": "^9.24.0",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -33,9 +33,9 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.0",
|
||||
"eslint": "^9.23.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"eslint": "^9.24.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ http {
|
||||
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 $http_x_forwarded_proto;
|
||||
client_max_body_size 32M;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.12.0",
|
||||
"version": "1.15.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -38,22 +38,22 @@
|
||||
"@semantic-release/github": "^11.0.1",
|
||||
"@semantic-release/npm": "^12.0.1",
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@turbo/gen": "^2.4.4",
|
||||
"@turbo/gen": "^2.5.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"@vitest/ui": "^3.0.9",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/ui": "^3.1.1",
|
||||
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jsdom": "^26.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"semantic-release": "^24.2.3",
|
||||
"testcontainers": "^10.23.0",
|
||||
"turbo": "^2.4.4",
|
||||
"typescript": "^5.8.2",
|
||||
"testcontainers": "^10.24.1",
|
||||
"turbo": "^2.5.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.9"
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.7.0",
|
||||
"packageManager": "pnpm@10.8.0",
|
||||
"engines": {
|
||||
"node": ">=22.14.0"
|
||||
},
|
||||
@@ -70,7 +70,7 @@
|
||||
"tree-sitter-json"
|
||||
],
|
||||
"overrides": {
|
||||
"proxmox-api>undici": "7.6.0"
|
||||
"proxmox-api>undici": "7.7.0"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"pretty-print-error": "patches/pretty-print-error.patch"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -40,25 +40,25 @@
|
||||
"@homarr/request-handler": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.1.0",
|
||||
"@trpc/client": "^11.0.1",
|
||||
"@trpc/react-query": "^11.0.1",
|
||||
"@trpc/server": "^11.0.1",
|
||||
"@kubernetes/client-node": "^1.1.2",
|
||||
"@trpc/client": "^11.0.4",
|
||||
"@trpc/react-query": "^11.0.4",
|
||||
"@trpc/server": "^11.0.4",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "15.2.4",
|
||||
"next": "15.2.5",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^2.1.3",
|
||||
"trpc-to-openapi": "^2.1.5",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint": "^9.24.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import { asc, createId, eq, inArray, like } from "@homarr/db";
|
||||
import { apps } from "@homarr/db/schema";
|
||||
import { selectAppSchema } from "@homarr/db/validationSchemas";
|
||||
import { getIconForName } from "@homarr/icons";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { appCreateManySchema, appEditSchema, appManageSchema } from "@homarr/validation/app";
|
||||
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
||||
|
||||
import { convertIntersectionToZodObject } from "../schema-merger";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||
@@ -15,7 +16,7 @@ const defaultIcon = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@mas
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
getPaginated: protectedProcedure
|
||||
.input(validation.common.paginated)
|
||||
.input(paginatedSchema)
|
||||
.output(z.object({ items: z.array(selectAppSchema), totalCount: z.number() }))
|
||||
.meta({ openapi: { method: "GET", path: "/api/apps/paginated", tags: ["apps"], protect: true } })
|
||||
.query(async ({ input, ctx }) => {
|
||||
@@ -83,7 +84,7 @@ export const appRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
byId: publicProcedure
|
||||
.input(validation.common.byId)
|
||||
.input(byIdSchema)
|
||||
.output(selectAppSchema)
|
||||
.meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
|
||||
.query(async ({ ctx, input }) => {
|
||||
@@ -115,7 +116,7 @@ export const appRouter = createTRPCRouter({
|
||||
}),
|
||||
create: permissionRequiredProcedure
|
||||
.requiresPermission("app-create")
|
||||
.input(validation.app.manage)
|
||||
.input(appManageSchema)
|
||||
.output(z.object({ appId: z.string() }))
|
||||
.meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } })
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -133,7 +134,7 @@ export const appRouter = createTRPCRouter({
|
||||
}),
|
||||
createMany: permissionRequiredProcedure
|
||||
.requiresPermission("app-create")
|
||||
.input(validation.app.createMany)
|
||||
.input(appCreateManySchema)
|
||||
.output(z.void())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db.insert(apps).values(
|
||||
@@ -148,7 +149,7 @@ export const appRouter = createTRPCRouter({
|
||||
}),
|
||||
update: permissionRequiredProcedure
|
||||
.requiresPermission("app-modify-all")
|
||||
.input(convertIntersectionToZodObject(validation.app.edit))
|
||||
.input(convertIntersectionToZodObject(appEditSchema))
|
||||
.output(z.void())
|
||||
.meta({ openapi: { method: "PATCH", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -178,7 +179,7 @@ export const appRouter = createTRPCRouter({
|
||||
.requiresPermission("app-full-all")
|
||||
.output(z.void())
|
||||
.meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
|
||||
.input(validation.common.byId)
|
||||
.input(byIdSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db.delete(apps).where(eq(apps.id, input.id));
|
||||
}),
|
||||
|
||||
+351
-264
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
import { z } from "zod";
|
||||
|
||||
import { constructBoardPermissions, isWidgetRestricted } from "@homarr/auth/shared";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import type { DeviceType } from "@homarr/common/server";
|
||||
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db";
|
||||
import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
|
||||
@@ -37,10 +37,22 @@ import {
|
||||
import { importOldmarrAsync } from "@homarr/old-import";
|
||||
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
||||
import { oldmarrConfigSchema } from "@homarr/old-schema";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||
import { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation";
|
||||
import {
|
||||
boardByNameSchema,
|
||||
boardChangeVisibilitySchema,
|
||||
boardCreateSchema,
|
||||
boardDuplicateSchema,
|
||||
boardRenameSchema,
|
||||
boardSaveLayoutsSchema,
|
||||
boardSavePartialSettingsSchema,
|
||||
boardSavePermissionsSchema,
|
||||
boardSaveSchema,
|
||||
} from "@homarr/validation/board";
|
||||
import { byIdSchema } from "@homarr/validation/common";
|
||||
import { zodUnionFromArray } from "@homarr/validation/enums";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
|
||||
|
||||
import { widgetImports } from "../../../widgets/src";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||
import { generateResponsiveGridFor } from "./board/grid-algorithm";
|
||||
@@ -248,7 +260,7 @@ export const boardRouter = createTRPCRouter({
|
||||
}),
|
||||
createBoard: permissionRequiredProcedure
|
||||
.requiresPermission("board-create")
|
||||
.input(validation.board.create)
|
||||
.input(boardCreateSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const boardId = createId();
|
||||
|
||||
@@ -292,7 +304,7 @@ export const boardRouter = createTRPCRouter({
|
||||
}),
|
||||
duplicateBoard: permissionRequiredProcedure
|
||||
.requiresPermission("board-create")
|
||||
.input(validation.board.duplicate)
|
||||
.input(boardDuplicateSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
|
||||
await noBoardWithSimilarNameAsync(ctx.db, input.name);
|
||||
@@ -324,13 +336,6 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const { sections: boardSections, items: boardItems, layouts: boardLayouts, ...boardProps } = board;
|
||||
const allowedBoardItems = boardItems.filter((item) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: widgetImports[item.kind].definition,
|
||||
user: ctx.session.user,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
});
|
||||
|
||||
const newBoardId = createId();
|
||||
|
||||
@@ -378,8 +383,8 @@ export const boardRouter = createTRPCRouter({
|
||||
),
|
||||
);
|
||||
|
||||
const itemMap = new Map<string, string>(allowedBoardItems.map((item) => [item.id, createId()]));
|
||||
const itemsToInsert: InferInsertModel<typeof items>[] = allowedBoardItems.map(
|
||||
const itemMap = new Map<string, string>(boardItems.map((item) => [item.id, createId()]));
|
||||
const itemsToInsert: InferInsertModel<typeof items>[] = boardItems.map(
|
||||
({ integrations: _, layouts: _layouts, ...item }) => ({
|
||||
...item,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
@@ -388,7 +393,7 @@ export const boardRouter = createTRPCRouter({
|
||||
}),
|
||||
);
|
||||
|
||||
const itemLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = allowedBoardItems.flatMap((item) =>
|
||||
const itemLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = boardItems.flatMap((item) =>
|
||||
item.layouts.map(
|
||||
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
|
||||
...layoutSection,
|
||||
@@ -421,7 +426,7 @@ export const boardRouter = createTRPCRouter({
|
||||
)
|
||||
.then((result) => result.map((row) => row.id));
|
||||
|
||||
const itemIntegrationsToInsert = allowedBoardItems.flatMap((item) =>
|
||||
const itemIntegrationsToInsert = boardItems.flatMap((item) =>
|
||||
item.integrations
|
||||
// Restrict integrations to only those the user has access to
|
||||
.filter(({ integrationId }) => integrationIdsWithAccess.includes(integrationId) || hasAccessForAll)
|
||||
@@ -514,34 +519,32 @@ export const boardRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}),
|
||||
renameBoard: protectedProcedure.input(validation.board.rename).mutation(async ({ ctx, input }) => {
|
||||
renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||
|
||||
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
|
||||
|
||||
await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id));
|
||||
}),
|
||||
changeBoardVisibility: protectedProcedure
|
||||
.input(validation.board.changeVisibility)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
|
||||
changeBoardVisibility: protectedProcedure.input(boardChangeVisibilitySchema).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
|
||||
|
||||
if (
|
||||
input.visibility !== "public" &&
|
||||
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Cannot make home board private",
|
||||
});
|
||||
}
|
||||
if (
|
||||
input.visibility !== "public" &&
|
||||
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Cannot make home board private",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(boards)
|
||||
.set({ isPublic: input.visibility === "public" })
|
||||
.where(eq(boards.id, input.id));
|
||||
}),
|
||||
await ctx.db
|
||||
.update(boards)
|
||||
.set({ isPublic: input.visibility === "public" })
|
||||
.where(eq(boards.id, input.id));
|
||||
}),
|
||||
deleteBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||
|
||||
@@ -580,13 +583,13 @@ export const boardRouter = createTRPCRouter({
|
||||
|
||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
}),
|
||||
getBoardByName: publicProcedure.input(validation.board.byName).query(async ({ input, ctx }) => {
|
||||
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
|
||||
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
|
||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
||||
|
||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
}),
|
||||
saveLayouts: protectedProcedure.input(validation.board.saveLayouts).mutation(async ({ ctx, input }) => {
|
||||
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||
|
||||
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||
@@ -712,7 +715,7 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
savePartialBoardSettings: protectedProcedure
|
||||
.input(validation.board.savePartialSettings.and(z.object({ id: z.string() })))
|
||||
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||
|
||||
@@ -746,145 +749,110 @@ export const boardRouter = createTRPCRouter({
|
||||
})
|
||||
.where(eq(boards.id, input.id));
|
||||
}),
|
||||
saveBoard: protectedProcedure.input(validation.board.save).mutation(async ({ input, ctx }) => {
|
||||
saveBoard: protectedProcedure.input(boardSaveSchema).mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||
|
||||
const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
const sectionsToInsert = addedSections.map(
|
||||
(section): InferInsertModel<typeof sections> => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
|
||||
xOffset: section.kind === "dynamic" ? null : 0,
|
||||
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const sectionLayoutsToInsert = addedSections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.flatMap((section) =>
|
||||
section.layouts.map(
|
||||
(sectionLayout): InferInsertModel<typeof sectionLayouts> => ({
|
||||
layoutId: sectionLayout.layoutId,
|
||||
sectionId: section.id,
|
||||
parentSectionId: sectionLayout.parentSectionId,
|
||||
height: sectionLayout.height,
|
||||
width: sectionLayout.width,
|
||||
xOffset: sectionLayout.xOffset,
|
||||
yOffset: sectionLayout.yOffset,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const addedItems = filterAddedItems(input.items, dbBoard.items).filter((item) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: widgetImports[item.kind].definition,
|
||||
user: ctx.session.user,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
});
|
||||
const itemsToInsert = addedItems.map(
|
||||
(item): InferInsertModel<typeof items> => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
boardId: dbBoard.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const itemLayoutsToInsert = addedItems.flatMap((item) =>
|
||||
item.layouts.map(
|
||||
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
|
||||
layoutId: layoutSection.layoutId,
|
||||
sectionId: layoutSection.sectionId,
|
||||
itemId: item.id,
|
||||
height: layoutSection.height,
|
||||
width: layoutSection.width,
|
||||
xOffset: layoutSection.xOffset,
|
||||
yOffset: layoutSection.yOffset,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
const integrationItemsToInsert = addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
}));
|
||||
|
||||
const updatedItems = filterUpdatedItems(input.items, dbBoard.items).filter((item) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: widgetImports[item.kind].definition,
|
||||
user: ctx.session.user,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
});
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
const removedItems = filterRemovedItems(input.items, dbBoard.items).filter((item) => {
|
||||
return !isWidgetRestricted({
|
||||
definition: widgetImports[item.kind].definition,
|
||||
user: ctx.session.user,
|
||||
check: (level) => level !== "none",
|
||||
});
|
||||
});
|
||||
const itemIdsToRemove = removedItems.map((item) => item.id);
|
||||
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIdsToRemove = removedSections.map((section) => section.id);
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
if (sectionsToInsert.length > 0) {
|
||||
await transaction.insert(schema.sections).values(sectionsToInsert);
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
|
||||
if (addedSections.length > 0) {
|
||||
await transaction.insert(schema.sections).values(
|
||||
addedSections.map((section) => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
|
||||
xOffset: section.kind === "dynamic" ? null : 0,
|
||||
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
);
|
||||
|
||||
if (addedSections.some((section) => section.kind === "dynamic")) {
|
||||
await transaction.insert(schema.sectionLayouts).values(
|
||||
addedSections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.flatMap((section) =>
|
||||
section.layouts.map(
|
||||
(sectionLayout): InferInsertModel<typeof schema.sectionLayouts> => ({
|
||||
layoutId: sectionLayout.layoutId,
|
||||
sectionId: section.id,
|
||||
parentSectionId: sectionLayout.parentSectionId,
|
||||
height: sectionLayout.height,
|
||||
width: sectionLayout.width,
|
||||
xOffset: sectionLayout.xOffset,
|
||||
yOffset: sectionLayout.yOffset,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionLayoutsToInsert.length > 0) {
|
||||
await transaction.insert(schema.sectionLayouts).values(sectionLayoutsToInsert);
|
||||
const addedItems = filterAddedItems(input.items, dbBoard.items);
|
||||
|
||||
if (addedItems.length > 0) {
|
||||
await transaction.insert(schema.items).values(
|
||||
addedItems.map((item) => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
);
|
||||
await transaction.insert(schema.itemLayouts).values(
|
||||
addedItems.flatMap((item) =>
|
||||
item.layouts.map(
|
||||
(layoutSection): InferInsertModel<typeof schema.itemLayouts> => ({
|
||||
layoutId: layoutSection.layoutId,
|
||||
sectionId: layoutSection.sectionId,
|
||||
itemId: item.id,
|
||||
height: layoutSection.height,
|
||||
width: layoutSection.width,
|
||||
xOffset: layoutSection.xOffset,
|
||||
yOffset: layoutSection.yOffset,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (itemsToInsert.length > 0) {
|
||||
await transaction.insert(schema.items).values(itemsToInsert);
|
||||
}
|
||||
if (itemLayoutsToInsert.length > 0) {
|
||||
await transaction.insert(schema.itemLayouts).values(itemLayoutsToInsert);
|
||||
const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
await transaction.insert(schema.integrationItems).values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (integrationItemsToInsert.length > 0) {
|
||||
await transaction.insert(schema.integrationItems).values(integrationItemsToInsert);
|
||||
}
|
||||
const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
|
||||
|
||||
for (const item of updatedItems) {
|
||||
await transaction
|
||||
@@ -915,6 +883,8 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||
await transaction
|
||||
@@ -948,6 +918,15 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
for (const relation of removedIntegrationRelations) {
|
||||
await transaction
|
||||
.delete(schema.integrationItems)
|
||||
@@ -959,36 +938,134 @@ export const boardRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
if (itemIdsToRemove.length > 0) {
|
||||
await transaction.delete(schema.items).where(inArray(schema.items.id, itemIdsToRemove));
|
||||
const removedItems = filterRemovedItems(input.items, dbBoard.items);
|
||||
|
||||
const itemIds = removedItems.map((item) => item.id);
|
||||
if (itemIds.length > 0) {
|
||||
await transaction.delete(schema.items).where(inArray(schema.items.id, itemIds));
|
||||
}
|
||||
|
||||
if (sectionIdsToRemove.length > 0) {
|
||||
await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIdsToRemove));
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIds = removedSections.map((section) => section.id);
|
||||
|
||||
if (sectionIds.length > 0) {
|
||||
await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIds));
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
if (sectionsToInsert.length > 0) {
|
||||
transaction.insert(sections).values(sectionsToInsert).run();
|
||||
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
|
||||
|
||||
if (addedSections.length > 0) {
|
||||
transaction
|
||||
.insert(sections)
|
||||
.values(
|
||||
addedSections.map((section) => ({
|
||||
id: section.id,
|
||||
kind: section.kind,
|
||||
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
|
||||
xOffset: section.kind === "dynamic" ? null : 0,
|
||||
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
|
||||
name: "name" in section ? section.name : null,
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
|
||||
if (addedSections.some((section) => section.kind === "dynamic")) {
|
||||
transaction
|
||||
.insert(sectionLayouts)
|
||||
.values(
|
||||
addedSections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.flatMap((section) =>
|
||||
section.layouts.map(
|
||||
(sectionLayout): InferInsertModel<typeof sectionLayouts> => ({
|
||||
layoutId: sectionLayout.layoutId,
|
||||
sectionId: section.id,
|
||||
parentSectionId: sectionLayout.parentSectionId,
|
||||
height: sectionLayout.height,
|
||||
width: sectionLayout.width,
|
||||
xOffset: sectionLayout.xOffset,
|
||||
yOffset: sectionLayout.yOffset,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionLayoutsToInsert.length > 0) {
|
||||
transaction.insert(sectionLayouts).values(sectionLayoutsToInsert).run();
|
||||
const addedItems = filterAddedItems(input.items, dbBoard.items);
|
||||
|
||||
if (addedItems.length > 0) {
|
||||
transaction
|
||||
.insert(items)
|
||||
.values(
|
||||
addedItems.map((item) => ({
|
||||
id: item.id,
|
||||
kind: item.kind,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
boardId: dbBoard.id,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
transaction
|
||||
.insert(itemLayouts)
|
||||
.values(
|
||||
addedItems.flatMap((item) =>
|
||||
item.layouts.map(
|
||||
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
|
||||
layoutId: layoutSection.layoutId,
|
||||
sectionId: layoutSection.sectionId,
|
||||
itemId: item.id,
|
||||
height: layoutSection.height,
|
||||
width: layoutSection.width,
|
||||
xOffset: layoutSection.xOffset,
|
||||
yOffset: layoutSection.yOffset,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
if (itemsToInsert.length > 0) {
|
||||
transaction.insert(items).values(itemsToInsert).run();
|
||||
const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
|
||||
integrationIds.map((integrationId) => ({
|
||||
integrationId,
|
||||
itemId,
|
||||
})),
|
||||
);
|
||||
const addedIntegrationRelations = inputIntegrationRelations.filter(
|
||||
(inputRelation) =>
|
||||
!dbIntegrationRelations.some(
|
||||
(dbRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
if (addedIntegrationRelations.length > 0) {
|
||||
transaction
|
||||
.insert(integrationItems)
|
||||
.values(
|
||||
addedIntegrationRelations.map((relation) => ({
|
||||
itemId: relation.itemId,
|
||||
integrationId: relation.integrationId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
if (itemLayoutsToInsert.length > 0) {
|
||||
transaction.insert(itemLayouts).values(itemLayoutsToInsert).run();
|
||||
}
|
||||
|
||||
if (integrationItemsToInsert.length > 0) {
|
||||
transaction.insert(integrationItems).values(integrationItemsToInsert).run();
|
||||
}
|
||||
const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
|
||||
|
||||
for (const item of updatedItems) {
|
||||
transaction
|
||||
@@ -1016,6 +1093,8 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
|
||||
|
||||
for (const section of updatedSections) {
|
||||
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
|
||||
transaction
|
||||
@@ -1048,6 +1127,15 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const removedIntegrationRelations = dbIntegrationRelations.filter(
|
||||
(dbRelation) =>
|
||||
!inputIntegrationRelations.some(
|
||||
(inputRelation) =>
|
||||
dbRelation.itemId === inputRelation.itemId &&
|
||||
dbRelation.integrationId === inputRelation.integrationId,
|
||||
),
|
||||
);
|
||||
|
||||
for (const relation of removedIntegrationRelations) {
|
||||
transaction
|
||||
.delete(integrationItems)
|
||||
@@ -1060,19 +1148,24 @@ export const boardRouter = createTRPCRouter({
|
||||
.run();
|
||||
}
|
||||
|
||||
if (itemIdsToRemove.length > 0) {
|
||||
transaction.delete(items).where(inArray(items.id, itemIdsToRemove)).run();
|
||||
const removedItems = filterRemovedItems(input.items, dbBoard.items);
|
||||
|
||||
const itemIds = removedItems.map((item) => item.id);
|
||||
if (itemIds.length > 0) {
|
||||
transaction.delete(items).where(inArray(items.id, itemIds)).run();
|
||||
}
|
||||
|
||||
if (sectionIdsToRemove.length > 0) {
|
||||
transaction.delete(sections).where(inArray(sections.id, sectionIdsToRemove)).run();
|
||||
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
|
||||
const sectionIds = removedSections.map((section) => section.id);
|
||||
|
||||
if (sectionIds.length > 0) {
|
||||
transaction.delete(sections).where(inArray(sections.id, sectionIds)).run();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
getBoardPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => {
|
||||
getBoardPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
|
||||
|
||||
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
|
||||
@@ -1143,99 +1236,93 @@ export const boardRouter = createTRPCRouter({
|
||||
}),
|
||||
};
|
||||
}),
|
||||
saveUserBoardPermissions: protectedProcedure
|
||||
.input(validation.board.savePermissions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
|
||||
saveUserBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(schema.boardUserPermissions)
|
||||
.where(eq(boardUserPermissions.boardId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(schema.boardUserPermissions).values(
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
await transaction.delete(schema.boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(schema.boardUserPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
userId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run();
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
transaction
|
||||
.insert(boardUserPermissions)
|
||||
.values(
|
||||
input.permissions.map((permission) => ({
|
||||
userId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run();
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
transaction
|
||||
.insert(boardUserPermissions)
|
||||
.values(
|
||||
input.permissions.map((permission) => ({
|
||||
userId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
});
|
||||
},
|
||||
});
|
||||
}),
|
||||
saveGroupBoardPermissions: protectedProcedure
|
||||
.input(validation.board.savePermissions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
|
||||
)
|
||||
.run();
|
||||
});
|
||||
},
|
||||
});
|
||||
}),
|
||||
saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
|
||||
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(schema.boardGroupPermissions)
|
||||
.where(eq(boardGroupPermissions.boardId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(schema.boardGroupPermissions).values(
|
||||
await handleTransactionsAsync(ctx.db, {
|
||||
async handleAsync(db, schema) {
|
||||
await db.transaction(async (transaction) => {
|
||||
await transaction
|
||||
.delete(schema.boardGroupPermissions)
|
||||
.where(eq(boardGroupPermissions.boardId, input.entityId));
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
await transaction.insert(schema.boardGroupPermissions).values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run();
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
transaction
|
||||
.insert(boardGroupPermissions)
|
||||
.values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
handleSync(db) {
|
||||
db.transaction((transaction) => {
|
||||
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run();
|
||||
if (input.permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
transaction
|
||||
.insert(boardGroupPermissions)
|
||||
.values(
|
||||
input.permissions.map((permission) => ({
|
||||
groupId: permission.principalId,
|
||||
permission: permission.permission,
|
||||
boardId: input.entityId,
|
||||
})),
|
||||
)
|
||||
.run();
|
||||
});
|
||||
},
|
||||
});
|
||||
}),
|
||||
)
|
||||
.run();
|
||||
});
|
||||
},
|
||||
});
|
||||
}),
|
||||
importOldmarrConfig: permissionRequiredProcedure
|
||||
.requiresPermission("board-create")
|
||||
.input(importJsonFileSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const content = await input.file.text();
|
||||
const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content));
|
||||
await importOldmarrAsync(ctx.db, oldmarr, input.configuration, ctx.session);
|
||||
await importOldmarrAsync(ctx.db, oldmarr, input.configuration);
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
import { zfd } from "zod-form-data";
|
||||
|
||||
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
|
||||
import { superRefineCertificateFile, validation } from "@homarr/validation";
|
||||
import { certificateValidFileNameSchema, superRefineCertificateFile } from "@homarr/validation/certificates";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||
|
||||
@@ -20,7 +20,7 @@ export const certificateRouter = createTRPCRouter({
|
||||
}),
|
||||
removeCertificate: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(z.object({ fileName: validation.certificates.validFileNameSchema }))
|
||||
.input(z.object({ fileName: certificateValidFileNameSchema }))
|
||||
.mutation(async ({ input }) => {
|
||||
await removeCustomRootCertificateAsync(input.fileName);
|
||||
}),
|
||||
|
||||
@@ -6,7 +6,15 @@ import { and, createId, eq, handleTransactionsAsync, like, not } from "@homarr/d
|
||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
|
||||
import { everyoneGroup } from "@homarr/definitions";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
||||
import {
|
||||
groupCreateSchema,
|
||||
groupSavePartialSettingsSchema,
|
||||
groupSavePermissionsSchema,
|
||||
groupSavePositionsSchema,
|
||||
groupUpdateSchema,
|
||||
groupUserSchema,
|
||||
} from "@homarr/validation/group";
|
||||
|
||||
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc";
|
||||
import { throwIfCredentialsDisabled } from "./invite/checks";
|
||||
@@ -39,7 +47,7 @@ export const groupRouter = createTRPCRouter({
|
||||
|
||||
getPaginated: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.common.paginated)
|
||||
.input(paginatedSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
|
||||
const groupCount = await ctx.db.$count(groups, whereQuery);
|
||||
@@ -74,7 +82,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
getById: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.common.byId)
|
||||
.input(byIdSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const group = await ctx.db.query.groups.findFirst({
|
||||
where: eq(groups.id, input.id),
|
||||
@@ -169,7 +177,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
createInitialExternalGroup: onboardingProcedure
|
||||
.requiresStep("group")
|
||||
.input(validation.group.create)
|
||||
.input(groupCreateSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
||||
|
||||
@@ -191,7 +199,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
createGroup: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.create)
|
||||
.input(groupCreateSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
|
||||
|
||||
@@ -209,7 +217,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
updateGroup: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.update)
|
||||
.input(groupUpdateSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
||||
@@ -225,7 +233,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
savePartialSettings: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.savePartialSettings)
|
||||
.input(groupSavePartialSettingsSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
|
||||
@@ -239,7 +247,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
savePositions: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.savePositions)
|
||||
.input(groupSavePositionsSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const positions = input.positions.map((id, index) => ({ id, position: index + 1 }));
|
||||
|
||||
@@ -262,7 +270,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
savePermissions: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.savePermissions)
|
||||
.input(groupSavePermissionsSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
|
||||
@@ -277,7 +285,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
transferOwnership: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.groupUser)
|
||||
.input(groupUserSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||
@@ -291,7 +299,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
deleteGroup: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.common.byId)
|
||||
.input(byIdSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.id);
|
||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
|
||||
@@ -300,7 +308,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
addMember: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.groupUser)
|
||||
.input(groupUserSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||
@@ -324,7 +332,7 @@ export const groupRouter = createTRPCRouter({
|
||||
}),
|
||||
removeMember: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.input(validation.group.groupUser)
|
||||
.input(groupUserSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
|
||||
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { and, like } from "@homarr/db";
|
||||
import { icons } from "@homarr/db/schema";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { iconsFindSchema } from "@homarr/validation/icons";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
|
||||
export const iconsRouter = createTRPCRouter({
|
||||
findIcons: publicProcedure.input(validation.icons.findIcons).query(async ({ ctx, input }) => {
|
||||
findIcons: publicProcedure.input(iconsFindSchema).query(async ({ ctx, input }) => {
|
||||
return {
|
||||
icons: await ctx.db.query.iconRepositories.findMany({
|
||||
with: {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const importRouter = createTRPCRouter({
|
||||
.requiresStep("import")
|
||||
.input(importInitialOldmarrInputSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await importInitialOldmarrAsync(ctx.db, input, ctx.session);
|
||||
await importInitialOldmarrAsync(ctx.db, input);
|
||||
await nextOnboardingStepAsync(ctx.db, undefined);
|
||||
}),
|
||||
});
|
||||
@@ -24,7 +24,12 @@ import {
|
||||
integrationSecretKindObject,
|
||||
} from "@homarr/definitions";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { byIdSchema } from "@homarr/validation/common";
|
||||
import {
|
||||
integrationCreateSchema,
|
||||
integrationSavePermissionsSchema,
|
||||
integrationUpdateSchema,
|
||||
} from "@homarr/validation/integration";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
@@ -141,7 +146,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}),
|
||||
byId: protectedProcedure.input(validation.integration.byId).query(async ({ ctx, input }) => {
|
||||
byId: protectedProcedure.input(byIdSchema).query(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, input.id),
|
||||
@@ -178,7 +183,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
}),
|
||||
create: permissionRequiredProcedure
|
||||
.requiresPermission("integration-create")
|
||||
.input(validation.integration.create)
|
||||
.input(integrationCreateSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await testConnectionAsync({
|
||||
id: "new",
|
||||
@@ -221,7 +226,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => {
|
||||
update: protectedProcedure.input(integrationUpdateSchema).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
@@ -282,7 +287,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
}),
|
||||
delete: protectedProcedure.input(validation.integration.delete).mutation(async ({ ctx, input }) => {
|
||||
delete: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
@@ -298,7 +303,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
|
||||
await ctx.db.delete(integrations).where(eq(integrations.id, input.id));
|
||||
}),
|
||||
getIntegrationPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => {
|
||||
getIntegrationPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
|
||||
|
||||
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
|
||||
@@ -370,7 +375,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
saveUserIntegrationPermissions: protectedProcedure
|
||||
.input(validation.integration.savePermissions)
|
||||
.input(integrationSavePermissionsSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
|
||||
|
||||
@@ -416,7 +421,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
saveGroupIntegrationPermissions: protectedProcedure
|
||||
.input(validation.integration.savePermissions)
|
||||
.input(integrationSavePermissionsSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
|
||||
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
import type { z } from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { fetchWithTimeout } from "@homarr/common";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
|
||||
const citySchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
country: z.string().optional(),
|
||||
country_code: z.string().optional(),
|
||||
latitude: z.number(),
|
||||
longitude: z.number(),
|
||||
population: z.number().optional(),
|
||||
});
|
||||
|
||||
export const locationSearchCityInput = z.object({
|
||||
query: z.string(),
|
||||
});
|
||||
|
||||
export const locationSearchCityOutput = z
|
||||
.object({
|
||||
results: z.array(citySchema),
|
||||
})
|
||||
.or(
|
||||
z
|
||||
.object({
|
||||
generationtime_ms: z.number(),
|
||||
})
|
||||
.refine((data) => Object.keys(data).length === 1, { message: "Invalid response" })
|
||||
.transform(() => ({ results: [] })), // We fallback to empty array if no results
|
||||
);
|
||||
|
||||
export const locationRouter = createTRPCRouter({
|
||||
searchCity: publicProcedure
|
||||
.input(validation.location.searchCity.input)
|
||||
.output(validation.location.searchCity.output)
|
||||
.input(locationSearchCityInput)
|
||||
.output(locationSearchCityOutput)
|
||||
.query(async ({ input }) => {
|
||||
const res = await fetchWithTimeout(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`);
|
||||
return (await res.json()) as z.infer<typeof validation.location.searchCity.output>;
|
||||
return (await res.json()) as z.infer<typeof locationSearchCityOutput>;
|
||||
}),
|
||||
});
|
||||
@@ -5,14 +5,15 @@ import type { InferInsertModel } from "@homarr/db";
|
||||
import { and, createId, desc, eq, like } from "@homarr/db";
|
||||
import { iconRepositories, icons, medias } from "@homarr/db/schema";
|
||||
import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
|
||||
import { mediaUploadSchema } from "@homarr/validation/media";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
|
||||
|
||||
export const mediaRouter = createTRPCRouter({
|
||||
getPaginated: protectedProcedure
|
||||
.input(
|
||||
validation.common.paginated.and(
|
||||
paginatedSchema.and(
|
||||
z.object({ includeFromAllUsers: z.boolean().default(false), search: z.string().trim().default("") }),
|
||||
),
|
||||
)
|
||||
@@ -51,7 +52,7 @@ export const mediaRouter = createTRPCRouter({
|
||||
}),
|
||||
uploadMedia: permissionRequiredProcedure
|
||||
.requiresPermission("media-upload")
|
||||
.input(validation.media.uploadMedia)
|
||||
.input(mediaUploadSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const content = Buffer.from(await input.file.arrayBuffer());
|
||||
const id = createId();
|
||||
@@ -82,7 +83,7 @@ export const mediaRouter = createTRPCRouter({
|
||||
|
||||
return id;
|
||||
}),
|
||||
deleteMedia: protectedProcedure.input(validation.common.byId).mutation(async ({ ctx, input }) => {
|
||||
deleteMedia: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => {
|
||||
const dbMedia = await ctx.db.query.medias.findFirst({
|
||||
where: eq(medias.id, input.id),
|
||||
columns: {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { onboarding } from "@homarr/db/schema";
|
||||
import { onboardingSteps } from "@homarr/definitions";
|
||||
import { zodEnumFromArray } from "@homarr/validation";
|
||||
import { zodEnumFromArray } from "@homarr/validation/enums";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries";
|
||||
|
||||
@@ -5,13 +5,15 @@ import { asc, createId, eq, like } from "@homarr/db";
|
||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import { searchEngines, users } from "@homarr/db/schema";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
|
||||
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
|
||||
export const searchEngineRouter = createTRPCRouter({
|
||||
getPaginated: protectedProcedure.input(validation.common.paginated).query(async ({ input, ctx }) => {
|
||||
getPaginated: protectedProcedure.input(paginatedSchema).query(async ({ input, ctx }) => {
|
||||
const whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined;
|
||||
const searchEngineCount = await ctx.db.$count(searchEngines, whereQuery);
|
||||
|
||||
@@ -41,7 +43,7 @@ export const searchEngineRouter = createTRPCRouter({
|
||||
.then((engines) => engines.map((engine) => ({ value: engine.id, label: engine.name })));
|
||||
}),
|
||||
|
||||
byId: protectedProcedure.input(validation.common.byId).query(async ({ ctx, input }) => {
|
||||
byId: protectedProcedure.input(byIdSchema).query(async ({ ctx, input }) => {
|
||||
const searchEngine = await ctx.db.query.searchEngines.findFirst({
|
||||
where: eq(searchEngines.id, input.id),
|
||||
});
|
||||
@@ -115,7 +117,7 @@ export const searchEngineRouter = createTRPCRouter({
|
||||
|
||||
return null;
|
||||
}),
|
||||
search: protectedProcedure.input(validation.common.search).query(async ({ ctx, input }) => {
|
||||
search: protectedProcedure.input(searchSchema).query(async ({ ctx, input }) => {
|
||||
return await ctx.db.query.searchEngines.findMany({
|
||||
where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`),
|
||||
with: {
|
||||
@@ -132,21 +134,21 @@ export const searchEngineRouter = createTRPCRouter({
|
||||
}),
|
||||
getMediaRequestOptions: protectedProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
|
||||
.input(validation.common.mediaRequestOptions)
|
||||
.input(mediaRequestOptionsSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
||||
}),
|
||||
requestMedia: protectedProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
|
||||
.input(validation.common.requestMedia)
|
||||
.input(mediaRequestRequestSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
|
||||
}),
|
||||
create: permissionRequiredProcedure
|
||||
.requiresPermission("search-engine-create")
|
||||
.input(validation.searchEngine.manage)
|
||||
.input(searchEngineManageSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db.insert(searchEngines).values({
|
||||
id: createId(),
|
||||
@@ -161,7 +163,7 @@ export const searchEngineRouter = createTRPCRouter({
|
||||
}),
|
||||
update: permissionRequiredProcedure
|
||||
.requiresPermission("search-engine-modify-all")
|
||||
.input(validation.searchEngine.edit)
|
||||
.input(searchEngineEditSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const searchEngine = await ctx.db.query.searchEngines.findFirst({
|
||||
where: eq(searchEngines.id, input.id),
|
||||
@@ -188,7 +190,7 @@ export const searchEngineRouter = createTRPCRouter({
|
||||
}),
|
||||
delete: permissionRequiredProcedure
|
||||
.requiresPermission("search-engine-full-all")
|
||||
.input(validation.common.byId)
|
||||
.input(byIdSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.db
|
||||
.update(users)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
import { defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { settingsInitSchema } from "@homarr/validation/settings";
|
||||
|
||||
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
|
||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||
@@ -32,7 +32,7 @@ export const serverSettingsRouter = createTRPCRouter({
|
||||
}),
|
||||
initSettings: onboardingProcedure
|
||||
.requiresStep("settings")
|
||||
.input(validation.settings.init)
|
||||
.input(settingsInitSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await updateServerSettingByKeyAsync(ctx.db, "analytics", input.analytics);
|
||||
await updateServerSettingByKeyAsync(ctx.db, "crawlingAndIndexing", input.crawlingAndIndexing);
|
||||
|
||||
@@ -10,7 +10,20 @@ import { selectUserSchema } from "@homarr/db/validationSchemas";
|
||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { byIdSchema } from "@homarr/validation/common";
|
||||
import type { userBaseCreateSchema } from "@homarr/validation/user";
|
||||
import {
|
||||
userChangeColorSchemeSchema,
|
||||
userChangeHomeBoardsSchema,
|
||||
userChangePasswordApiSchema,
|
||||
userChangeSearchPreferencesSchema,
|
||||
userCreateSchema,
|
||||
userEditProfileSchema,
|
||||
userFirstDayOfWeekSchema,
|
||||
userInitSchema,
|
||||
userPingIconsEnabledSchema,
|
||||
userRegistrationApiSchema,
|
||||
} from "@homarr/validation/user";
|
||||
|
||||
import { convertIntersectionToZodObject } from "../schema-merger";
|
||||
import {
|
||||
@@ -28,7 +41,7 @@ import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from
|
||||
export const userRouter = createTRPCRouter({
|
||||
initUser: onboardingProcedure
|
||||
.requiresStep("user")
|
||||
.input(validation.user.init)
|
||||
.input(userInitSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
throwIfCredentialsDisabled();
|
||||
|
||||
@@ -52,7 +65,7 @@ export const userRouter = createTRPCRouter({
|
||||
await nextOnboardingStepAsync(ctx.db, undefined);
|
||||
}),
|
||||
register: publicProcedure
|
||||
.input(validation.user.registrationApi)
|
||||
.input(userRegistrationApiSchema)
|
||||
.output(z.void())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
throwIfCredentialsDisabled();
|
||||
@@ -82,7 +95,7 @@ export const userRouter = createTRPCRouter({
|
||||
create: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
.meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } })
|
||||
.input(validation.user.create)
|
||||
.input(userCreateSchema)
|
||||
.output(z.void())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
throwIfCredentialsDisabled();
|
||||
@@ -259,7 +272,7 @@ export const userRouter = createTRPCRouter({
|
||||
return user;
|
||||
}),
|
||||
editProfile: protectedProcedure
|
||||
.input(validation.user.editProfile)
|
||||
.input(userEditProfileSchema)
|
||||
.output(z.void())
|
||||
.meta({ openapi: { method: "PUT", path: "/api/users/profile", tags: ["users"], protect: true } })
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -318,7 +331,7 @@ export const userRouter = createTRPCRouter({
|
||||
await ctx.db.delete(users).where(eq(users.id, input.userId));
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.input(validation.user.changePasswordApi)
|
||||
.input(userChangePasswordApiSchema)
|
||||
.output(z.void())
|
||||
.meta({ openapi: { method: "PATCH", path: "/api/users/{userId}/changePassword", tags: ["users"], protect: true } })
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -384,7 +397,7 @@ export const userRouter = createTRPCRouter({
|
||||
.where(eq(users.id, input.userId));
|
||||
}),
|
||||
changeHomeBoards: protectedProcedure
|
||||
.input(convertIntersectionToZodObject(validation.user.changeHomeBoards.and(z.object({ userId: z.string() }))))
|
||||
.input(convertIntersectionToZodObject(userChangeHomeBoardsSchema.and(z.object({ userId: z.string() }))))
|
||||
.output(z.void())
|
||||
.meta({ openapi: { method: "PATCH", path: "/api/users/changeHome", tags: ["users"], protect: true } })
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -430,7 +443,7 @@ export const userRouter = createTRPCRouter({
|
||||
changeDefaultSearchEngine: protectedProcedure
|
||||
.input(
|
||||
convertIntersectionToZodObject(
|
||||
validation.user.changeSearchPreferences.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })),
|
||||
userChangeSearchPreferencesSchema.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })),
|
||||
),
|
||||
)
|
||||
.output(z.void())
|
||||
@@ -457,7 +470,7 @@ export const userRouter = createTRPCRouter({
|
||||
await changeSearchPreferencesAsync(ctx.db, ctx.session, input);
|
||||
}),
|
||||
changeColorScheme: protectedProcedure
|
||||
.input(validation.user.changeColorScheme)
|
||||
.input(userChangeColorSchemeSchema)
|
||||
.output(z.void())
|
||||
.meta({ openapi: { method: "PATCH", path: "/api/users/changeScheme", tags: ["users"], protect: true } })
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -469,7 +482,7 @@ export const userRouter = createTRPCRouter({
|
||||
.where(eq(users.id, ctx.session.user.id));
|
||||
}),
|
||||
changePingIconsEnabled: protectedProcedure
|
||||
.input(validation.user.pingIconsEnabled.and(validation.common.byId))
|
||||
.input(userPingIconsEnabledSchema.and(byIdSchema))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// Only admins can change other users ping icons enabled
|
||||
if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) {
|
||||
@@ -487,7 +500,7 @@ export const userRouter = createTRPCRouter({
|
||||
.where(eq(users.id, ctx.session.user.id));
|
||||
}),
|
||||
changeFirstDayOfWeek: protectedProcedure
|
||||
.input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId)))
|
||||
.input(convertIntersectionToZodObject(userFirstDayOfWeekSchema.and(byIdSchema)))
|
||||
.output(z.void())
|
||||
.meta({ openapi: { method: "PATCH", path: "/api/users/firstDayOfWeek", tags: ["users"], protect: true } })
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -522,7 +535,7 @@ export const userRouter = createTRPCRouter({
|
||||
}),
|
||||
});
|
||||
|
||||
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof validation.user.baseCreate>, "groupIds">) => {
|
||||
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof userBaseCreateSchema>, "groupIds">) => {
|
||||
const salt = await createSaltAsync();
|
||||
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { Modify } from "@homarr/common/types";
|
||||
import { eq } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
|
||||
|
||||
export const changeSearchPreferencesInputSchema = validation.user.changeSearchPreferences.and(
|
||||
export const changeSearchPreferencesInputSchema = userChangeSearchPreferencesSchema.and(
|
||||
z.object({ userId: z.string() }),
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { mediaRequestsRouter } from "./media-requests";
|
||||
import { mediaServerRouter } from "./media-server";
|
||||
import { mediaTranscodingRouter } from "./media-transcoding";
|
||||
import { minecraftRouter } from "./minecraft";
|
||||
import { networkControllerRouter } from "./network-controller";
|
||||
import { notebookRouter } from "./notebook";
|
||||
import { optionsRouter } from "./options";
|
||||
import { rssFeedRouter } from "./rssFeed";
|
||||
@@ -33,4 +34,5 @@ export const widgetRouter = createTRPCRouter({
|
||||
mediaTranscoding: mediaTranscodingRouter,
|
||||
minecraft: minecraftRouter,
|
||||
options: optionsRouter,
|
||||
networkController: networkControllerRouter,
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { paginatedSchema } from "@homarr/validation/common";
|
||||
|
||||
import type { IntegrationAction } from "../../middlewares/integration";
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
@@ -12,7 +12,7 @@ const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) =>
|
||||
export const mediaTranscodingRouter = createTRPCRouter({
|
||||
getDataAsync: publicProcedure
|
||||
.unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
|
||||
.input(validation.common.paginated.pick({ page: true, pageSize: true }))
|
||||
.input(paginatedSchema.pick({ page: true, pageSize: true }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {
|
||||
pageOffset: input.page,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import type { Integration } from "@homarr/db/schema";
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { NetworkControllerSummary } from "@homarr/integrations/types";
|
||||
import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const networkControllerRouter = createTRPCRouter({
|
||||
summary: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
|
||||
.query(async ({ ctx }) => {
|
||||
const results = await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const innerHandler = networkControllerRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
|
||||
return {
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
},
|
||||
summary: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
}),
|
||||
|
||||
subscribeToSummary: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{
|
||||
integration: Modify<Integration, { kind: IntegrationKindByCategory<"networkController"> }>;
|
||||
summary: NetworkControllerSummary;
|
||||
}>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
for (const integrationWithSecrets of ctx.integrations) {
|
||||
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||
const innerHandler = networkControllerRequestHandler.handler(integrationWithSecrets, {});
|
||||
const unsubscribe = innerHandler.subscribe((summary) => {
|
||||
emit.next({
|
||||
integration,
|
||||
summary,
|
||||
});
|
||||
});
|
||||
unsubscribes.push(unsubscribe);
|
||||
}
|
||||
return () => {
|
||||
unsubscribes.forEach((unsubscribe) => {
|
||||
unsubscribe();
|
||||
});
|
||||
};
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -3,9 +3,10 @@ import SuperJSON from "superjson";
|
||||
import { z } from "zod";
|
||||
|
||||
import { eq } from "@homarr/db";
|
||||
import { items } from "@homarr/db/schema";
|
||||
import { boards, items } from "@homarr/db/schema";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "../../trpc";
|
||||
import { throwIfActionForbiddenAsync } from "../board/board-access";
|
||||
|
||||
export const notebookRouter = createTRPCRouter({
|
||||
updateContent: protectedProcedure
|
||||
@@ -17,6 +18,8 @@ export const notebookRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.boardId), "modify");
|
||||
|
||||
const item = await ctx.db.query.items.findFirst({
|
||||
where: eq(items.id, input.itemId),
|
||||
});
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { fetchWithTimeout } from "@homarr/common";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
const atLocationInput = z.object({
|
||||
longitude: z.number(),
|
||||
latitude: z.number(),
|
||||
});
|
||||
|
||||
const atLocationOutput = z.object({
|
||||
current_weather: z.object({
|
||||
weathercode: z.number(),
|
||||
temperature: z.number(),
|
||||
windspeed: z.number(),
|
||||
}),
|
||||
daily: z.object({
|
||||
time: z.array(z.string()),
|
||||
weathercode: z.array(z.number()),
|
||||
temperature_2m_max: z.array(z.number()),
|
||||
temperature_2m_min: z.array(z.number()),
|
||||
sunrise: z.array(z.string()),
|
||||
sunset: z.array(z.string()),
|
||||
wind_speed_10m_max: z.array(z.number()),
|
||||
wind_gusts_10m_max: z.array(z.number()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const weatherRouter = createTRPCRouter({
|
||||
atLocation: publicProcedure.input(validation.widget.weather.atLocationInput).query(async ({ input }) => {
|
||||
atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => {
|
||||
const res = await fetchWithTimeout(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max¤t_weather=true&timezone=auto`,
|
||||
);
|
||||
const json: unknown = await res.json();
|
||||
const weather = await validation.widget.weather.atLocationOutput.parseAsync(json);
|
||||
const weather = await atLocationOutput.parseAsync(json);
|
||||
return {
|
||||
current: weather.current_weather,
|
||||
daily: weather.daily.time.map((value, index) => {
|
||||
|
||||
@@ -34,12 +34,12 @@
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cookies": "^0.9.1",
|
||||
"ldapts": "7.3.3",
|
||||
"next": "15.2.4",
|
||||
"ldapts": "7.4.0",
|
||||
"next": "15.2.5",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,8 +48,8 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/cookies": "0.9.0",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint": "^9.24.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./board-permissions";
|
||||
export * from "./integration-permissions";
|
||||
export * from "./widget-restriction";
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
import type { WidgetDefinition } from "../../widgets/src";
|
||||
import type { RestrictionLevel } from "../../widgets/src/definition";
|
||||
|
||||
export const isWidgetRestricted = <TDefinition extends WidgetDefinition>(props: {
|
||||
definition: TDefinition;
|
||||
user: Session["user"] | null;
|
||||
check: (level: RestrictionLevel) => boolean;
|
||||
}) => {
|
||||
if (!("restrict" in props.definition)) return false;
|
||||
if (props.definition.restrict === undefined) return false;
|
||||
return props.check(props.definition.restrict({ user: props.user ?? null }));
|
||||
};
|
||||
@@ -5,11 +5,11 @@ import type { Database } from "@homarr/db";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { validation } from "@homarr/validation";
|
||||
import type { userSignInSchema } from "@homarr/validation/user";
|
||||
|
||||
export const authorizeWithBasicCredentialsAsync = async (
|
||||
db: Database,
|
||||
credentials: z.infer<typeof validation.user.signIn>,
|
||||
credentials: z.infer<typeof userSignInSchema>,
|
||||
) => {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: and(eq(users.name, credentials.name.toLowerCase()), eq(users.provider, "credentials")),
|
||||
|
||||
@@ -6,14 +6,14 @@ import type { Database, InferInsertModel } from "@homarr/db";
|
||||
import { and, createId, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { validation } from "@homarr/validation";
|
||||
import type { userSignInSchema } from "@homarr/validation/user";
|
||||
|
||||
import { env } from "../../../env";
|
||||
import { LdapClient } from "../ldap-client";
|
||||
|
||||
export const authorizeWithLdapCredentialsAsync = async (
|
||||
db: Database,
|
||||
credentials: z.infer<typeof validation.user.signIn>,
|
||||
credentials: z.infer<typeof userSignInSchema>,
|
||||
) => {
|
||||
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
|
||||
const client = new LdapClient();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type Credentials from "@auth/core/providers/credentials";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { validation } from "@homarr/validation";
|
||||
import { userSignInSchema } from "@homarr/validation/user";
|
||||
|
||||
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
|
||||
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
|
||||
@@ -15,7 +15,7 @@ export const createCredentialsConfiguration = (db: Database) =>
|
||||
name: "Credentials",
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
async authorize(credentials) {
|
||||
const data = await validation.user.signIn.parseAsync(credentials);
|
||||
const data = await userSignInSchema.parseAsync(credentials);
|
||||
|
||||
return await authorizeWithBasicCredentialsAsync(db, data);
|
||||
},
|
||||
@@ -28,7 +28,7 @@ export const createLdapConfiguration = (db: Database) =>
|
||||
name: "Ldap",
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
async authorize(credentials) {
|
||||
const data = await validation.user.signIn.parseAsync(credentials);
|
||||
const data = await userSignInSchema.parseAsync(credentials);
|
||||
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
|
||||
},
|
||||
}) satisfies CredentialsConfiguration;
|
||||
@@ -25,14 +25,14 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,13 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"undici": "7.6.0"
|
||||
"undici": "7.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Agent } from "node:https";
|
||||
import { Agent as HttpsAgent } from "node:https";
|
||||
import path from "node:path";
|
||||
import { rootCertificates } from "node:tls";
|
||||
import axios from "axios";
|
||||
@@ -70,12 +70,16 @@ export const createCertificateAgentAsync = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const createAxiosCertificateInstanceAsync = async () => {
|
||||
export const createHttpsAgentAsync = async () => {
|
||||
const customCertificates = await loadCustomRootCertificatesAsync();
|
||||
return new HttpsAgent({
|
||||
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)),
|
||||
});
|
||||
};
|
||||
|
||||
export const createAxiosCertificateInstanceAsync = async () => {
|
||||
return axios.create({
|
||||
httpsAgent: new Agent({
|
||||
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)),
|
||||
}),
|
||||
httpsAgent: await createHttpsAgentAsync(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -27,13 +27,14 @@
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^16.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { command, string } from "@drizzle-team/brocli";
|
||||
|
||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||
import { generateSecureRandomToken } from "@homarr/common/server";
|
||||
import { and, count, createId, db, eq } from "@homarr/db";
|
||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
|
||||
import { usernameSchema } from "@homarr/validation/user";
|
||||
|
||||
export const recreateAdmin = command({
|
||||
name: "recreate-admin",
|
||||
desc: "Recreate credentials admin user if none exists anymore",
|
||||
options: {
|
||||
username: string("username").required().alias("u").desc("Name of the admin"),
|
||||
},
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
handler: async (options) => {
|
||||
if (!process.env.AUTH_PROVIDERS?.toLowerCase().includes("credentials")) {
|
||||
console.error("Credentials provider is not enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await usernameSchema.safeParseAsync(options.username);
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Invalid username:");
|
||||
console.error(result.error.errors.map((error) => `- ${error.message}`).join("\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
const totalCount = await db
|
||||
.select({
|
||||
count: count(),
|
||||
})
|
||||
.from(groupPermissions)
|
||||
.leftJoin(groupMembers, eq(groupMembers.groupId, groupPermissions.groupId))
|
||||
.leftJoin(users, eq(users.id, groupMembers.userId))
|
||||
.where(and(eq(groupPermissions.permission, "admin"), eq(users.provider, "credentials")))
|
||||
.then((rows) => rows.at(0)?.count ?? 0);
|
||||
|
||||
if (totalCount > 0) {
|
||||
console.error("Credentials admin user exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const existingUser = await db.query.users.findFirst({
|
||||
where: eq(users.name, result.data),
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
console.error("User with this name already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const temporaryGroupId = createId();
|
||||
|
||||
const maxPosition = await getMaxGroupPositionAsync(db);
|
||||
await db.insert(groups).values({
|
||||
id: temporaryGroupId,
|
||||
name: temporaryGroupId,
|
||||
position: maxPosition + 1,
|
||||
});
|
||||
|
||||
await db.insert(groupPermissions).values({
|
||||
groupId: temporaryGroupId,
|
||||
permission: "admin",
|
||||
});
|
||||
|
||||
const salt = await createSaltAsync();
|
||||
const password = generateSecureRandomToken(24);
|
||||
const hashedPassword = await hashPasswordAsync(password, salt);
|
||||
|
||||
const userId = createId();
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
name: result.data,
|
||||
provider: "credentials",
|
||||
password: hashedPassword,
|
||||
salt,
|
||||
});
|
||||
|
||||
await db.insert(groupMembers).values({
|
||||
groupId: temporaryGroupId,
|
||||
userId,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"We created a new admin user for you. Please keep in mind, that the admin group of it has a temporary name. You should change it to something more meaningful.",
|
||||
);
|
||||
console.log(`\tUsername: ${result.data}`);
|
||||
console.log(`\tPassword: ${password}`);
|
||||
console.log(`\tGroup: ${temporaryGroupId}`);
|
||||
console.log(""); // Empty line for better readability
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import { run } from "@drizzle-team/brocli";
|
||||
|
||||
import { fixUsernames } from "./commands/fix-usernames";
|
||||
import { recreateAdmin } from "./commands/recreate-admin";
|
||||
import { resetPassword } from "./commands/reset-password";
|
||||
|
||||
const commands = [resetPassword, fixUsernames];
|
||||
const commands = [resetPassword, fixUsernames, recreateAdmin];
|
||||
|
||||
void run(commands, {
|
||||
name: "homarr-cli",
|
||||
|
||||
@@ -30,17 +30,17 @@
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.2.4",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"undici": "7.6.0",
|
||||
"next": "15.2.5",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"undici": "7.7.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { objectKeys } from "@homarr/common";
|
||||
import type { JobGroupKeys } from "@homarr/cron-jobs";
|
||||
import { createSubPubChannel } from "@homarr/redis";
|
||||
import { zodEnumFromArray } from "@homarr/validation";
|
||||
import { zodEnumFromArray } from "@homarr/validation/enums";
|
||||
|
||||
export const cronJobRunnerChannel = createSubPubChannel<JobGroupKeys>("cron-job-runner", { persist: false });
|
||||
|
||||
@@ -23,6 +23,7 @@ export const cronJobs = {
|
||||
updateChecker: { preventManualExecution: false },
|
||||
mediaTranscoding: { preventManualExecution: false },
|
||||
minecraftServerStatus: { preventManualExecution: false },
|
||||
networkController: { preventManualExecution: false },
|
||||
} satisfies Record<JobGroupKeys, { preventManualExecution?: boolean }>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { mediaOrganizerJob } from "./jobs/integrations/media-organizer";
|
||||
import { mediaRequestListJob, mediaRequestStatsJob } from "./jobs/integrations/media-requests";
|
||||
import { mediaServerJob } from "./jobs/integrations/media-server";
|
||||
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
|
||||
import { networkControllerJob } from "./jobs/integrations/network-controller";
|
||||
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
||||
import { pingJob } from "./jobs/ping";
|
||||
import { rssFeedsJob } from "./jobs/rss-feeds";
|
||||
@@ -34,6 +35,7 @@ export const jobGroup = createCronJobGroup({
|
||||
updateChecker: updateCheckerJob,
|
||||
mediaTranscoding: mediaTranscodingJob,
|
||||
minecraftServerStatus: minecraftServerStatusJob,
|
||||
networkController: networkControllerJob,
|
||||
});
|
||||
|
||||
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
||||
@@ -0,0 +1,14 @@
|
||||
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
|
||||
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
|
||||
import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller";
|
||||
|
||||
import { createCronJob } from "../../lib";
|
||||
|
||||
export const networkControllerJob = createCronJob("networkController", EVERY_MINUTE).withCallback(
|
||||
createRequestIntegrationJobHandler(networkControllerRequestHandler.handler, {
|
||||
widgetKinds: ["networkControllerSummary"],
|
||||
getInput: {
|
||||
networkControllerSummary: () => ({}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -44,9 +44,9 @@
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@testcontainers/mysql": "^10.23.0",
|
||||
"@testcontainers/mysql": "^10.24.1",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-kit": "^0.30.6",
|
||||
@@ -58,11 +58,11 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint": "^9.24.0",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -379,6 +379,362 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/1.11.0/widgets/rss"
|
||||
| "/docs/1.11.0/widgets/video"
|
||||
| "/docs/1.11.0/widgets/weather"
|
||||
| "/docs/1.12.0/tags"
|
||||
| "/docs/1.12.0/tags/active-directory"
|
||||
| "/docs/1.12.0/tags/ad-guard"
|
||||
| "/docs/1.12.0/tags/ad-guard-home"
|
||||
| "/docs/1.12.0/tags/administration"
|
||||
| "/docs/1.12.0/tags/advanced"
|
||||
| "/docs/1.12.0/tags/analytics"
|
||||
| "/docs/1.12.0/tags/api"
|
||||
| "/docs/1.12.0/tags/apps"
|
||||
| "/docs/1.12.0/tags/banner"
|
||||
| "/docs/1.12.0/tags/blocking"
|
||||
| "/docs/1.12.0/tags/boards"
|
||||
| "/docs/1.12.0/tags/bookmark"
|
||||
| "/docs/1.12.0/tags/bookmarks"
|
||||
| "/docs/1.12.0/tags/caddy"
|
||||
| "/docs/1.12.0/tags/certificates"
|
||||
| "/docs/1.12.0/tags/checklist"
|
||||
| "/docs/1.12.0/tags/code"
|
||||
| "/docs/1.12.0/tags/community"
|
||||
| "/docs/1.12.0/tags/configuration"
|
||||
| "/docs/1.12.0/tags/connections"
|
||||
| "/docs/1.12.0/tags/customization"
|
||||
| "/docs/1.12.0/tags/data-sources"
|
||||
| "/docs/1.12.0/tags/database"
|
||||
| "/docs/1.12.0/tags/developer"
|
||||
| "/docs/1.12.0/tags/development"
|
||||
| "/docs/1.12.0/tags/dns"
|
||||
| "/docs/1.12.0/tags/docker"
|
||||
| "/docs/1.12.0/tags/donation"
|
||||
| "/docs/1.12.0/tags/edit-mode"
|
||||
| "/docs/1.12.0/tags/env"
|
||||
| "/docs/1.12.0/tags/environment-variables"
|
||||
| "/docs/1.12.0/tags/feeds"
|
||||
| "/docs/1.12.0/tags/finance"
|
||||
| "/docs/1.12.0/tags/getting-started"
|
||||
| "/docs/1.12.0/tags/google"
|
||||
| "/docs/1.12.0/tags/grafana"
|
||||
| "/docs/1.12.0/tags/groups"
|
||||
| "/docs/1.12.0/tags/hardware"
|
||||
| "/docs/1.12.0/tags/health"
|
||||
| "/docs/1.12.0/tags/help"
|
||||
| "/docs/1.12.0/tags/icon-picker"
|
||||
| "/docs/1.12.0/tags/icon-repositories"
|
||||
| "/docs/1.12.0/tags/icons"
|
||||
| "/docs/1.12.0/tags/iframe"
|
||||
| "/docs/1.12.0/tags/images"
|
||||
| "/docs/1.12.0/tags/installation"
|
||||
| "/docs/1.12.0/tags/integrade"
|
||||
| "/docs/1.12.0/tags/integration"
|
||||
| "/docs/1.12.0/tags/integrations"
|
||||
| "/docs/1.12.0/tags/interface"
|
||||
| "/docs/1.12.0/tags/jellyserr"
|
||||
| "/docs/1.12.0/tags/layout"
|
||||
| "/docs/1.12.0/tags/ldap"
|
||||
| "/docs/1.12.0/tags/links"
|
||||
| "/docs/1.12.0/tags/lists"
|
||||
| "/docs/1.12.0/tags/management"
|
||||
| "/docs/1.12.0/tags/market"
|
||||
| "/docs/1.12.0/tags/media"
|
||||
| "/docs/1.12.0/tags/minecraft"
|
||||
| "/docs/1.12.0/tags/monitoring"
|
||||
| "/docs/1.12.0/tags/news"
|
||||
| "/docs/1.12.0/tags/notebook"
|
||||
| "/docs/1.12.0/tags/notes"
|
||||
| "/docs/1.12.0/tags/oidc"
|
||||
| "/docs/1.12.0/tags/open-collective"
|
||||
| "/docs/1.12.0/tags/open-media-vault"
|
||||
| "/docs/1.12.0/tags/overseerr"
|
||||
| "/docs/1.12.0/tags/permissions"
|
||||
| "/docs/1.12.0/tags/pgid"
|
||||
| "/docs/1.12.0/tags/pi-hole"
|
||||
| "/docs/1.12.0/tags/ping"
|
||||
| "/docs/1.12.0/tags/programming"
|
||||
| "/docs/1.12.0/tags/proxmox"
|
||||
| "/docs/1.12.0/tags/proxy"
|
||||
| "/docs/1.12.0/tags/puid"
|
||||
| "/docs/1.12.0/tags/responsive"
|
||||
| "/docs/1.12.0/tags/roles"
|
||||
| "/docs/1.12.0/tags/rss"
|
||||
| "/docs/1.12.0/tags/search"
|
||||
| "/docs/1.12.0/tags/search-engines"
|
||||
| "/docs/1.12.0/tags/security"
|
||||
| "/docs/1.12.0/tags/self-signed"
|
||||
| "/docs/1.12.0/tags/seo"
|
||||
| "/docs/1.12.0/tags/server"
|
||||
| "/docs/1.12.0/tags/settings"
|
||||
| "/docs/1.12.0/tags/sinkhole"
|
||||
| "/docs/1.12.0/tags/sso"
|
||||
| "/docs/1.12.0/tags/stocks"
|
||||
| "/docs/1.12.0/tags/system"
|
||||
| "/docs/1.12.0/tags/table"
|
||||
| "/docs/1.12.0/tags/technical-documentation"
|
||||
| "/docs/1.12.0/tags/text"
|
||||
| "/docs/1.12.0/tags/torrent"
|
||||
| "/docs/1.12.0/tags/traefik"
|
||||
| "/docs/1.12.0/tags/translations"
|
||||
| "/docs/1.12.0/tags/unraid"
|
||||
| "/docs/1.12.0/tags/uploads"
|
||||
| "/docs/1.12.0/tags/usenet"
|
||||
| "/docs/1.12.0/tags/users"
|
||||
| "/docs/1.12.0/tags/variables"
|
||||
| "/docs/1.12.0/tags/widgets"
|
||||
| "/docs/1.12.0/advanced/command-line"
|
||||
| "/docs/1.12.0/advanced/command-line/fix-usernames"
|
||||
| "/docs/1.12.0/advanced/command-line/password-recovery"
|
||||
| "/docs/1.12.0/advanced/development/getting-started"
|
||||
| "/docs/1.12.0/advanced/development/kubernetes"
|
||||
| "/docs/1.12.0/advanced/environment-variables"
|
||||
| "/docs/1.12.0/advanced/icons"
|
||||
| "/docs/1.12.0/advanced/keyboard-shortcuts"
|
||||
| "/docs/1.12.0/advanced/proxy"
|
||||
| "/docs/1.12.0/advanced/running-as-different-user"
|
||||
| "/docs/1.12.0/advanced/single-sign-on"
|
||||
| "/docs/1.12.0/category/advanced"
|
||||
| "/docs/1.12.0/category/community"
|
||||
| "/docs/1.12.0/category/developer-guides"
|
||||
| "/docs/1.12.0/category/getting-started"
|
||||
| "/docs/1.12.0/category/installation"
|
||||
| "/docs/1.12.0/category/installation-1"
|
||||
| "/docs/1.12.0/category/integrations"
|
||||
| "/docs/1.12.0/category/management"
|
||||
| "/docs/1.12.0/category/widgets"
|
||||
| "/docs/1.12.0/community/donate"
|
||||
| "/docs/1.12.0/community/faq"
|
||||
| "/docs/1.12.0/community/get-in-touch"
|
||||
| "/docs/1.12.0/community/license"
|
||||
| "/docs/1.12.0/community/translations"
|
||||
| "/docs/1.12.0/getting-started"
|
||||
| "/docs/1.12.0/getting-started/after-the-installation"
|
||||
| "/docs/1.12.0/getting-started/glossary"
|
||||
| "/docs/1.12.0/getting-started/installation/docker"
|
||||
| "/docs/1.12.0/getting-started/installation/easy-panel"
|
||||
| "/docs/1.12.0/getting-started/installation/helm"
|
||||
| "/docs/1.12.0/getting-started/installation/home-assistant"
|
||||
| "/docs/1.12.0/getting-started/installation/portainer"
|
||||
| "/docs/1.12.0/getting-started/installation/proxmox"
|
||||
| "/docs/1.12.0/getting-started/installation/qnap"
|
||||
| "/docs/1.12.0/getting-started/installation/railway"
|
||||
| "/docs/1.12.0/getting-started/installation/saltbox"
|
||||
| "/docs/1.12.0/getting-started/installation/source"
|
||||
| "/docs/1.12.0/getting-started/installation/synology"
|
||||
| "/docs/1.12.0/getting-started/installation/unraid"
|
||||
| "/docs/1.12.0/integrations/containers"
|
||||
| "/docs/1.12.0/integrations/dns"
|
||||
| "/docs/1.12.0/integrations/hardware"
|
||||
| "/docs/1.12.0/integrations/kubernetes"
|
||||
| "/docs/1.12.0/integrations/media-requester"
|
||||
| "/docs/1.12.0/integrations/media-server"
|
||||
| "/docs/1.12.0/integrations/servarr"
|
||||
| "/docs/1.12.0/integrations/torrent"
|
||||
| "/docs/1.12.0/integrations/usenet"
|
||||
| "/docs/1.12.0/management/api"
|
||||
| "/docs/1.12.0/management/apps"
|
||||
| "/docs/1.12.0/management/boards"
|
||||
| "/docs/1.12.0/management/certificates"
|
||||
| "/docs/1.12.0/management/integrations"
|
||||
| "/docs/1.12.0/management/media"
|
||||
| "/docs/1.12.0/management/search-engines"
|
||||
| "/docs/1.12.0/management/settings"
|
||||
| "/docs/1.12.0/management/users"
|
||||
| "/docs/1.12.0/widgets/bookmarks"
|
||||
| "/docs/1.12.0/widgets/calendar"
|
||||
| "/docs/1.12.0/widgets/clock"
|
||||
| "/docs/1.12.0/widgets/dns-hole"
|
||||
| "/docs/1.12.0/widgets/downloads"
|
||||
| "/docs/1.12.0/widgets/health-monitoring"
|
||||
| "/docs/1.12.0/widgets/home-assistant"
|
||||
| "/docs/1.12.0/widgets/iframe"
|
||||
| "/docs/1.12.0/widgets/indexer-manager"
|
||||
| "/docs/1.12.0/widgets/media-requests"
|
||||
| "/docs/1.12.0/widgets/media-server"
|
||||
| "/docs/1.12.0/widgets/minecraft-server-status"
|
||||
| "/docs/1.12.0/widgets/notebook"
|
||||
| "/docs/1.12.0/widgets/rss"
|
||||
| "/docs/1.12.0/widgets/stocks"
|
||||
| "/docs/1.12.0/widgets/video"
|
||||
| "/docs/1.12.0/widgets/weather"
|
||||
| "/docs/1.13.1/tags"
|
||||
| "/docs/1.13.1/tags/active-directory"
|
||||
| "/docs/1.13.1/tags/ad-guard"
|
||||
| "/docs/1.13.1/tags/ad-guard-home"
|
||||
| "/docs/1.13.1/tags/administration"
|
||||
| "/docs/1.13.1/tags/advanced"
|
||||
| "/docs/1.13.1/tags/analytics"
|
||||
| "/docs/1.13.1/tags/api"
|
||||
| "/docs/1.13.1/tags/apps"
|
||||
| "/docs/1.13.1/tags/banner"
|
||||
| "/docs/1.13.1/tags/blocking"
|
||||
| "/docs/1.13.1/tags/boards"
|
||||
| "/docs/1.13.1/tags/bookmark"
|
||||
| "/docs/1.13.1/tags/bookmarks"
|
||||
| "/docs/1.13.1/tags/caddy"
|
||||
| "/docs/1.13.1/tags/certificates"
|
||||
| "/docs/1.13.1/tags/checklist"
|
||||
| "/docs/1.13.1/tags/code"
|
||||
| "/docs/1.13.1/tags/community"
|
||||
| "/docs/1.13.1/tags/configuration"
|
||||
| "/docs/1.13.1/tags/connections"
|
||||
| "/docs/1.13.1/tags/customization"
|
||||
| "/docs/1.13.1/tags/data-sources"
|
||||
| "/docs/1.13.1/tags/database"
|
||||
| "/docs/1.13.1/tags/developer"
|
||||
| "/docs/1.13.1/tags/development"
|
||||
| "/docs/1.13.1/tags/dns"
|
||||
| "/docs/1.13.1/tags/docker"
|
||||
| "/docs/1.13.1/tags/donation"
|
||||
| "/docs/1.13.1/tags/edit-mode"
|
||||
| "/docs/1.13.1/tags/env"
|
||||
| "/docs/1.13.1/tags/environment-variables"
|
||||
| "/docs/1.13.1/tags/feeds"
|
||||
| "/docs/1.13.1/tags/finance"
|
||||
| "/docs/1.13.1/tags/getting-started"
|
||||
| "/docs/1.13.1/tags/google"
|
||||
| "/docs/1.13.1/tags/grafana"
|
||||
| "/docs/1.13.1/tags/groups"
|
||||
| "/docs/1.13.1/tags/hardware"
|
||||
| "/docs/1.13.1/tags/health"
|
||||
| "/docs/1.13.1/tags/help"
|
||||
| "/docs/1.13.1/tags/icon-picker"
|
||||
| "/docs/1.13.1/tags/icon-repositories"
|
||||
| "/docs/1.13.1/tags/icons"
|
||||
| "/docs/1.13.1/tags/iframe"
|
||||
| "/docs/1.13.1/tags/images"
|
||||
| "/docs/1.13.1/tags/installation"
|
||||
| "/docs/1.13.1/tags/integrade"
|
||||
| "/docs/1.13.1/tags/integration"
|
||||
| "/docs/1.13.1/tags/integrations"
|
||||
| "/docs/1.13.1/tags/interface"
|
||||
| "/docs/1.13.1/tags/jellyserr"
|
||||
| "/docs/1.13.1/tags/layout"
|
||||
| "/docs/1.13.1/tags/ldap"
|
||||
| "/docs/1.13.1/tags/links"
|
||||
| "/docs/1.13.1/tags/lists"
|
||||
| "/docs/1.13.1/tags/management"
|
||||
| "/docs/1.13.1/tags/market"
|
||||
| "/docs/1.13.1/tags/media"
|
||||
| "/docs/1.13.1/tags/minecraft"
|
||||
| "/docs/1.13.1/tags/monitoring"
|
||||
| "/docs/1.13.1/tags/news"
|
||||
| "/docs/1.13.1/tags/notebook"
|
||||
| "/docs/1.13.1/tags/notes"
|
||||
| "/docs/1.13.1/tags/oidc"
|
||||
| "/docs/1.13.1/tags/open-collective"
|
||||
| "/docs/1.13.1/tags/open-media-vault"
|
||||
| "/docs/1.13.1/tags/overseerr"
|
||||
| "/docs/1.13.1/tags/permissions"
|
||||
| "/docs/1.13.1/tags/pgid"
|
||||
| "/docs/1.13.1/tags/pi-hole"
|
||||
| "/docs/1.13.1/tags/ping"
|
||||
| "/docs/1.13.1/tags/programming"
|
||||
| "/docs/1.13.1/tags/proxmox"
|
||||
| "/docs/1.13.1/tags/proxy"
|
||||
| "/docs/1.13.1/tags/puid"
|
||||
| "/docs/1.13.1/tags/responsive"
|
||||
| "/docs/1.13.1/tags/roles"
|
||||
| "/docs/1.13.1/tags/rss"
|
||||
| "/docs/1.13.1/tags/search"
|
||||
| "/docs/1.13.1/tags/search-engines"
|
||||
| "/docs/1.13.1/tags/security"
|
||||
| "/docs/1.13.1/tags/self-signed"
|
||||
| "/docs/1.13.1/tags/seo"
|
||||
| "/docs/1.13.1/tags/server"
|
||||
| "/docs/1.13.1/tags/settings"
|
||||
| "/docs/1.13.1/tags/sinkhole"
|
||||
| "/docs/1.13.1/tags/sso"
|
||||
| "/docs/1.13.1/tags/stocks"
|
||||
| "/docs/1.13.1/tags/system"
|
||||
| "/docs/1.13.1/tags/table"
|
||||
| "/docs/1.13.1/tags/technical-documentation"
|
||||
| "/docs/1.13.1/tags/text"
|
||||
| "/docs/1.13.1/tags/torrent"
|
||||
| "/docs/1.13.1/tags/traefik"
|
||||
| "/docs/1.13.1/tags/translations"
|
||||
| "/docs/1.13.1/tags/unraid"
|
||||
| "/docs/1.13.1/tags/uploads"
|
||||
| "/docs/1.13.1/tags/usenet"
|
||||
| "/docs/1.13.1/tags/users"
|
||||
| "/docs/1.13.1/tags/variables"
|
||||
| "/docs/1.13.1/tags/widgets"
|
||||
| "/docs/1.13.1/advanced/command-line"
|
||||
| "/docs/1.13.1/advanced/command-line/fix-usernames"
|
||||
| "/docs/1.13.1/advanced/command-line/password-recovery"
|
||||
| "/docs/1.13.1/advanced/development/getting-started"
|
||||
| "/docs/1.13.1/advanced/development/kubernetes"
|
||||
| "/docs/1.13.1/advanced/environment-variables"
|
||||
| "/docs/1.13.1/advanced/icons"
|
||||
| "/docs/1.13.1/advanced/keyboard-shortcuts"
|
||||
| "/docs/1.13.1/advanced/proxy"
|
||||
| "/docs/1.13.1/advanced/running-as-different-user"
|
||||
| "/docs/1.13.1/advanced/single-sign-on"
|
||||
| "/docs/1.13.1/advanced/styling"
|
||||
| "/docs/1.13.1/category/advanced"
|
||||
| "/docs/1.13.1/category/community"
|
||||
| "/docs/1.13.1/category/developer-guides"
|
||||
| "/docs/1.13.1/category/getting-started"
|
||||
| "/docs/1.13.1/category/installation"
|
||||
| "/docs/1.13.1/category/installation-1"
|
||||
| "/docs/1.13.1/category/integrations"
|
||||
| "/docs/1.13.1/category/management"
|
||||
| "/docs/1.13.1/category/widgets"
|
||||
| "/docs/1.13.1/community/donate"
|
||||
| "/docs/1.13.1/community/faq"
|
||||
| "/docs/1.13.1/community/get-in-touch"
|
||||
| "/docs/1.13.1/community/license"
|
||||
| "/docs/1.13.1/community/translations"
|
||||
| "/docs/1.13.1/getting-started"
|
||||
| "/docs/1.13.1/getting-started/after-the-installation"
|
||||
| "/docs/1.13.1/getting-started/glossary"
|
||||
| "/docs/1.13.1/getting-started/installation/docker"
|
||||
| "/docs/1.13.1/getting-started/installation/easy-panel"
|
||||
| "/docs/1.13.1/getting-started/installation/helm"
|
||||
| "/docs/1.13.1/getting-started/installation/home-assistant"
|
||||
| "/docs/1.13.1/getting-started/installation/portainer"
|
||||
| "/docs/1.13.1/getting-started/installation/proxmox"
|
||||
| "/docs/1.13.1/getting-started/installation/qnap"
|
||||
| "/docs/1.13.1/getting-started/installation/railway"
|
||||
| "/docs/1.13.1/getting-started/installation/saltbox"
|
||||
| "/docs/1.13.1/getting-started/installation/source"
|
||||
| "/docs/1.13.1/getting-started/installation/synology"
|
||||
| "/docs/1.13.1/getting-started/installation/unraid"
|
||||
| "/docs/1.13.1/integrations/cloud"
|
||||
| "/docs/1.13.1/integrations/containers"
|
||||
| "/docs/1.13.1/integrations/dns"
|
||||
| "/docs/1.13.1/integrations/hardware"
|
||||
| "/docs/1.13.1/integrations/kubernetes"
|
||||
| "/docs/1.13.1/integrations/media-requester"
|
||||
| "/docs/1.13.1/integrations/media-server"
|
||||
| "/docs/1.13.1/integrations/servarr"
|
||||
| "/docs/1.13.1/integrations/torrent"
|
||||
| "/docs/1.13.1/integrations/usenet"
|
||||
| "/docs/1.13.1/management/api"
|
||||
| "/docs/1.13.1/management/apps"
|
||||
| "/docs/1.13.1/management/boards"
|
||||
| "/docs/1.13.1/management/certificates"
|
||||
| "/docs/1.13.1/management/integrations"
|
||||
| "/docs/1.13.1/management/media"
|
||||
| "/docs/1.13.1/management/search-engines"
|
||||
| "/docs/1.13.1/management/settings"
|
||||
| "/docs/1.13.1/management/users"
|
||||
| "/docs/1.13.1/widgets/bookmarks"
|
||||
| "/docs/1.13.1/widgets/calendar"
|
||||
| "/docs/1.13.1/widgets/clock"
|
||||
| "/docs/1.13.1/widgets/dns-hole"
|
||||
| "/docs/1.13.1/widgets/downloads"
|
||||
| "/docs/1.13.1/widgets/health-monitoring"
|
||||
| "/docs/1.13.1/widgets/home-assistant"
|
||||
| "/docs/1.13.1/widgets/iframe"
|
||||
| "/docs/1.13.1/widgets/indexer-manager"
|
||||
| "/docs/1.13.1/widgets/media-requests"
|
||||
| "/docs/1.13.1/widgets/media-server"
|
||||
| "/docs/1.13.1/widgets/minecraft-server-status"
|
||||
| "/docs/1.13.1/widgets/notebook"
|
||||
| "/docs/1.13.1/widgets/rss"
|
||||
| "/docs/1.13.1/widgets/stocks"
|
||||
| "/docs/1.13.1/widgets/video"
|
||||
| "/docs/1.13.1/widgets/weather"
|
||||
| "/docs/next/tags"
|
||||
| "/docs/next/tags/active-directory"
|
||||
| "/docs/next/tags/ad-guard"
|
||||
@@ -671,6 +1027,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/advanced/proxy"
|
||||
| "/docs/advanced/running-as-different-user"
|
||||
| "/docs/advanced/single-sign-on"
|
||||
| "/docs/advanced/styling"
|
||||
| "/docs/category/advanced"
|
||||
| "/docs/category/community"
|
||||
| "/docs/category/developer-guides"
|
||||
@@ -700,6 +1057,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/getting-started/installation/source"
|
||||
| "/docs/getting-started/installation/synology"
|
||||
| "/docs/getting-started/installation/unraid"
|
||||
| "/docs/integrations/cloud"
|
||||
| "/docs/integrations/containers"
|
||||
| "/docs/integrations/dns"
|
||||
| "/docs/integrations/hardware"
|
||||
|
||||
@@ -157,6 +157,12 @@ export const integrationDefs = {
|
||||
category: ["calendar"],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg",
|
||||
},
|
||||
unifiController: {
|
||||
name: "Unifi Controller",
|
||||
secretKinds: [["username", "password"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
|
||||
category: ["networkController"],
|
||||
},
|
||||
} as const satisfies Record<string, integrationDefinition>;
|
||||
|
||||
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
|
||||
@@ -209,4 +215,5 @@ export type IntegrationCategory =
|
||||
| "indexerManager"
|
||||
| "healthMonitoring"
|
||||
| "search"
|
||||
| "mediaTranscoding";
|
||||
| "mediaTranscoding"
|
||||
| "networkController";
|
||||
@@ -17,6 +17,8 @@ export const widgetKinds = [
|
||||
"mediaRequests-requestStats",
|
||||
"mediaTranscoding",
|
||||
"minecraftServerStatus",
|
||||
"networkControllerSummary",
|
||||
"networkControllerStatus",
|
||||
"rssFeed",
|
||||
"bookmarks",
|
||||
"indexerManager",
|
||||
|
||||
@@ -25,14 +25,14 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"dockerode": "^4.0.4"
|
||||
"dockerode": "^4.0.5"
|
||||
},
|
||||
"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.36",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"@types/dockerode": "^3.3.37",
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
Vendored
+2
-2
@@ -30,7 +30,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,14 @@
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/form": "^7.17.3",
|
||||
"@mantine/form": "^7.17.4",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
import type { AnyZodObject, ZodDiscriminatedUnion, ZodEffects, ZodIntersection } from "zod";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { zodErrorMap } from "@homarr/validation/form";
|
||||
import { zodErrorMap } from "@homarr/validation/form/i18n";
|
||||
|
||||
export const useZodForm = <
|
||||
TSchema extends
|
||||
|
||||
@@ -29,15 +29,15 @@
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.3",
|
||||
"react": "19.0.0",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"react": "19.1.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.23.0",
|
||||
"typescript": "^5.8.2"
|
||||
"eslint": "^9.24.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,9 @@ interface IconPickerProps {
|
||||
error?: string | null;
|
||||
onFocus?: FocusEventHandler;
|
||||
onBlur?: FocusEventHandler;
|
||||
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
withAsterisk?: boolean;
|
||||
}
|
||||
|
||||
@@ -43,6 +46,8 @@ export const IconPicker = ({
|
||||
onFocus,
|
||||
onBlur,
|
||||
withAsterisk = true,
|
||||
label,
|
||||
placeholder,
|
||||
}: IconPickerProps) => {
|
||||
const [value, setValue] = useUncontrolled({
|
||||
value: propsValue,
|
||||
@@ -155,8 +160,8 @@ export const IconPicker = ({
|
||||
rightSectionPointerEvents="none"
|
||||
withAsterisk={withAsterisk}
|
||||
error={error}
|
||||
label={tCommon("iconPicker.label")}
|
||||
placeholder={tCommon("iconPicker.header", { countIcons: String(data?.countIcons ?? 0) })}
|
||||
label={label ?? tCommon("iconPicker.label")}
|
||||
placeholder={placeholder ?? tCommon("iconPicker.header", { countIcons: String(data?.countIcons ?? 0) })}
|
||||
/>
|
||||
{session?.user.permissions.includes("media-upload") && (
|
||||
<UploadMedia
|
||||
|
||||
@@ -8,7 +8,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import type { validation } from "@homarr/validation";
|
||||
import type { appManageSchema } from "@homarr/validation/app";
|
||||
|
||||
import { AppForm } from "./_form";
|
||||
|
||||
@@ -33,7 +33,7 @@ export const AppNewForm = ({
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(values: z.infer<typeof validation.app.manage>, redirect: boolean, afterSuccess?: () => void) => {
|
||||
(values: z.infer<typeof appManageSchema>, redirect: boolean, afterSuccess?: () => void) => {
|
||||
mutate(values, {
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
|
||||
Loaded 100 of 206 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user