diff --git a/README.md b/README.md index f106b66adde..716484ee042 100644 --- a/README.md +++ b/README.md @@ -53,34 +53,46 @@ Simplify the management of your server with Homarr - a sleek, modern dashboard t - šŸ¦ž Comprehensive built-in icon picker with over 7000 icons - šŸ³ Easy deployment with Docker, unRAID, and Synology - šŸš€ Compatible with any major consumer hardware (x86, Raspberry Pi, old laptops, ...) -- šŸ’µ Free and Open-Source - your data stays on your device. No telemetry data.

![Widgets & Integrations Section](docs/section-widgets-and-integrations.png) -Homarr has a [built-in collection of widgets and integrations](https://homarr.dev/docs/integrations/), that connect to your applications and enable you to control them directly from the dashboard. -Each widget and integration has a comprehensive documentation for your comfort. -Homarr will integrate with the following applications of yours: - -- šŸ“„ Torrent clients - - [Deluge](https://homarr.dev/docs/integrations/#deluge) - - [Transmission](https://homarr.dev/docs/integrations/#transmission) - - [qBittorent](https://homarr.dev/docs/integrations/#qbittorrent-integration) -- šŸ“„ Usenet clients - - [SABnzbd](https://homarr.dev/docs/integrations/#sabnzbd) - - [NZBGet](https://homarr.dev/docs/integrations/#nzbget) -- šŸ“š Media collection managers - - [Sonarr](https://homarr.dev/docs/integrations/#sonarr) - - [Radarr](https://homarr.dev/docs/integrations/#radarr) - - [Lidarr](https://homarr.dev/docs/integrations/#lidarr) - - [Readarr](https://homarr.dev/docs/integrations/#readarr) -- šŸŽžļø Media request managers - - [Overseerr](https://homarr.dev/docs/integrations/#overseerr--jellyseerr) - - [Jellyseerr](https://homarr.dev/docs/integrations/#overseerr--jellyseerr) -- šŸ”Œ [Dash.](https://homarr.dev/docs/integrations/#dash) -- šŸ³ [Docker](https://homarr.dev/docs/integrations/#docker) +Homarr has a [built-in collection of widgets and integrations](https://homarr.dev/docs/management/integrations/), that connect to your applications and enable you to control them directly from the dashboard. +Each widget and integration has a comprehensive documentation +Homarr will integrate with the following applications: + +šŸ“„ Torrent clients +- [Deluge](https://homarr.dev/docs/management/integrations/torrent-deluge) +- [Transmission](https://homarr.dev/docs/management/integrations/torrent-transmission) +- [qBittorent](https://homarr.dev/docs/management/integrations/torrent-qbittorrent) + +šŸ“„ Usenet clients +- [SABnzbd](https://homarr.dev/docs/management/integrations/usenet-sabnzbd) +- [NZBGet](https://homarr.dev/docs/management/integrations/usenet-nzbget) + +šŸ“ŗ Media servers +- [Plex](https://homarr.dev/docs/management/integrations/media-server-plex) +- [Jellyfin](https://homarr.dev/docs/management/integrations/media-server-jellyfin) + +šŸ“š Media collection managers +- [Sonarr](https://homarr.dev/docs/management/integrations/servarr-sonarr) +- [Radarr](https://homarr.dev/docs/management/integrations/servarr-radarr) +- [Lidarr](https://homarr.dev/docs/management/integrations/servarr-lidarr) +- [Readarr](https://homarr.dev/docs/management/integrations/servarr-readarr) + +šŸŽžļø Media request managers +- [Overseerr](https://homarr.dev/docs/management/integrations/media-requester/) +- [Jellyseerr](https://homarr.dev/docs/management/integrations/media-requester/) + +šŸš« DNS ad-blockers +- [Pihole](https://homarr.dev/docs/management/integrations/dns-pihole) +- [AdGuard Home](https://homarr.dev/docs/management/integrations/dns-adguard-home) + +Other integrations +- [šŸ”Œ Dash.](https://homarr.dev/docs/management/integrations/hardware-dash) +- [šŸ³ Docker](https://homarr.dev/docs/management/integrations/containers-docker) We're constantly adding new integrations and widgets, which will enhance your experience even further. @@ -123,3 +135,4 @@ You can also support us by helping with [translating the entire project](https:/ All contributions, regardless of their size or scope, are welcome and highly appreciated! Thank you ā¤ļø ![Alt](https://repobeats.axiom.co/api/embed/60a6f68f193faf831f64221bdf90782adec51c93.svg "Repobeats analytics image") +[![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss) diff --git a/next.config.js b/next.config.js index c87a8ebb2dd..191310ecf92 100644 --- a/next.config.js +++ b/next.config.js @@ -6,6 +6,11 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ }); module.exports = withBundleAnalyzer({ + webpack: (config) => { + // for dynamic loading of auth providers + config.experiments = { ...config.experiments, topLevelAwait: true }; + return config; + }, images: { domains: ['cdn.jsdelivr.net'], }, diff --git a/package.json b/package.json index 590c9572296..46b6adf7a66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homarr", - "version": "0.14.6", + "version": "0.15.0", "description": "Homarr - A homepage for your server.", "license": "MIT", "repository": { @@ -27,7 +27,6 @@ "db:migrate": "dotenv ts-node drizzle/migrate/migrate.ts ./drizzle" }, "dependencies": { - "@auth/drizzle-adapter": "^0.3.2", "@ctrl/deluge": "^4.1.0", "@ctrl/qbittorrent": "^6.0.0", "@ctrl/shared-torrent": "^4.1.1", @@ -92,9 +91,8 @@ "i18next": "^22.5.1", "immer": "^10.0.2", "js-file-download": "^0.4.12", + "ldapjs": "^3.0.5", "mantine-react-table": "^1.3.4", - "moment": "^2.29.4", - "moment-timezone": "^0.5.43", "next": "13.4.12", "next-auth": "^4.23.0", "next-i18next": "^14.0.0", @@ -123,6 +121,7 @@ "@types/better-sqlite3": "^7.6.5", "@types/cookies": "^0.7.7", "@types/dockerode": "^3.3.9", + "@types/ldapjs": "^3.0.2", "@types/node": "18.17.8", "@types/prismjs": "^1.26.0", "@types/react": "^18.2.11", diff --git a/public/locales/en/modules/indexer-manager.json b/public/locales/en/modules/indexer-manager.json new file mode 100644 index 00000000000..051593223e1 --- /dev/null +++ b/public/locales/en/modules/indexer-manager.json @@ -0,0 +1,19 @@ +{ + "descriptor": { + "name": "Indexer manager status", + "description": "Status about your indexers", + "settings": { + "title": "Indexer manager status" + } + }, + "indexersStatus": { + "title": "Indexer manager", + "testAllButton": "Test all" + }, + "errors": { + "general": { + "title": "Unable to find a indexer manager", + "text": "There was a problem connecting to your indexer manager. Please verify your configuration/integration(s)." + } + } + } \ No newline at end of file diff --git a/public/locales/en/modules/smart-home/entity-state.json b/public/locales/en/modules/smart-home/entity-state.json index c76a4fae7b7..f7eacbbb28c 100644 --- a/public/locales/en/modules/smart-home/entity-state.json +++ b/public/locales/en/modules/smart-home/entity-state.json @@ -9,12 +9,20 @@ "label": "Entity ID", "info": "Unique entity ID in Home Assistant. Copy by clicking on entity > Click on cog icon > Click on copy button at 'Entity ID'. Some custom entities may not be supported." }, + "appendUnit": { + "label": "Append unit of measurement", + "info": "Append the unit of measurement attribute to the entity state." + }, "automationId": { "label": "Optional automation ID", "info": "Your unique automation ID. Always starts with automation.XXXXX. If not set, widget will not be clickable and only display state. After click, entity state will be refreshed." }, "displayName": { "label": "Display name" + }, + "displayFriendlyName": { + "label": "Display friendly name", + "info": "Display friendly name from Home Assistant instead instead of display name" } } } diff --git a/public/locales/it/modules/rss.json b/public/locales/it/modules/rss.json index cbf20088d7e..69f64f95964 100644 --- a/public/locales/it/modules/rss.json +++ b/public/locales/it/modules/rss.json @@ -22,10 +22,10 @@ "label": "Ordina per data di pubblicazione (ascendente)" }, "sortPostsWithoutPublishDateToTheTop": { - "label": "" + "label": "Metti i post senza data di pubblicazione in alto" }, "maximumAmountOfPosts": { - "label": "" + "label": "Numero massimo di post" } }, "card": { diff --git a/public/locales/lv/common.json b/public/locales/lv/common.json index 527c8dc9757..d2480df6651 100644 --- a/public/locales/lv/common.json +++ b/public/locales/lv/common.json @@ -13,7 +13,7 @@ "previous": "IepriekŔējais", "confirm": "Apstipriniet", "enabled": "Iespējots", - "duplicate": "", + "duplicate": "Dublicēt", "disabled": "Atspējots", "enableAll": "Iespējot visu", "disableAll": "Atspējot visu", @@ -54,5 +54,5 @@ "height": "Augstums" }, "public": "Publisks", - "restricted": "" + "restricted": "Ierobežots" } \ No newline at end of file diff --git a/public/locales/lv/layout/element-selector/selector.json b/public/locales/lv/layout/element-selector/selector.json index 88354cbf32f..b1344cd5a39 100644 --- a/public/locales/lv/layout/element-selector/selector.json +++ b/public/locales/lv/layout/element-selector/selector.json @@ -22,5 +22,5 @@ "message": "Ir izveidota kategorija \"{{name}}\"" } }, - "importFromDocker": "" + "importFromDocker": "Importēt no Docker" } diff --git a/public/locales/lv/manage/boards.json b/public/locales/lv/manage/boards.json index 92593c986ac..19ba5f707f5 100644 --- a/public/locales/lv/manage/boards.json +++ b/public/locales/lv/manage/boards.json @@ -16,15 +16,15 @@ "label": "Neatgriezeniski dzēst", "disabled": "DzÄ“Å”ana atspējota, jo vecāki Homarr komponenti neļauj dzēst noklusējuma konfigurāciju. DzÄ“Å”ana bÅ«s iespējama nākotnē." }, - "duplicate": "", + "duplicate": "Dublicēt", "rename": { - "label": "", + "label": "Pārdēvēt", "modal": { - "title": "", + "title": "Pārdēvēt dēli {{name}}", "fields": { "name": { - "label": "", - "placeholder": "" + "label": "Jauns nosaukums", + "placeholder": "Jauns dēļa nosaukums" } } } diff --git a/public/locales/lv/manage/users.json b/public/locales/lv/manage/users.json index 1562ab6e62b..acbc0980515 100644 --- a/public/locales/lv/manage/users.json +++ b/public/locales/lv/manage/users.json @@ -6,10 +6,10 @@ }, "filter": { "roles": { - "all": "", - "normal": "", - "admin": "", - "owner": "" + "all": "Viss", + "normal": "Normāls", + "admin": "Administrators", + "owner": "ÄŖpaÅ”nieks" } }, "table": { diff --git a/public/locales/lv/manage/users/edit.json b/public/locales/lv/manage/users/edit.json index a190593eb57..493455f10d3 100644 --- a/public/locales/lv/manage/users/edit.json +++ b/public/locales/lv/manage/users/edit.json @@ -1,6 +1,6 @@ { - "metaTitle": "", - "back": "", + "metaTitle": "Lietotājs {{username}}", + "back": "Atgriezties uz lietotāju pārvaldÄ«bu", "sections": { "general": { "title": "VispārÄ«gi", @@ -14,40 +14,40 @@ } }, "security": { - "title": "", + "title": "DroŔība", "inputs": { "password": { - "label": "" + "label": "Jauna parole" }, "terminateExistingSessions": { - "label": "", - "description": "" + "label": "Pārtraukt esoŔās sesijas", + "description": "Piespiež lietotāju no jauna pieteikties savās ierÄ«cēs" }, "confirm": { "label": "Apstipriniet", - "description": "" + "description": "Parole tiks atjaunināta. Å o darbÄ«bu nevar atcelt." } } }, "roles": { - "title": "", - "currentRole": "", + "title": "Lomas", + "currentRole": "PaÅ”reizējā loma: ", "badges": { - "owner": "", - "admin": "", - "normal": "" + "owner": "ÄŖpaÅ”nieks", + "admin": "Administrators", + "normal": "Normāls" } }, "deletion": { - "title": "", + "title": "Konta dzÄ“Å”ana", "inputs": { "confirmUsername": { - "label": "", - "description": "" + "label": "Apstiprināt lietotājvārdu", + "description": "Ierakstiet lietotājvārdu, lai apstiprinātu dzÄ“Å”anu" }, "confirm": { "label": "Neatgriezeniski dzēst", - "description": "" + "description": "Es apzinos, ka Ŕī darbÄ«ba ir neatgriezeniska un visi konta dati tiks zaudēti." } } } diff --git a/public/locales/lv/modules/date.json b/public/locales/lv/modules/date.json index 6ece863de0a..6d2e2d2db25 100644 --- a/public/locales/lv/modules/date.json +++ b/public/locales/lv/modules/date.json @@ -5,11 +5,11 @@ "settings": { "title": "Datuma un Laika logrÄ«ka iestatÄ«jumi", "timezone": { - "label": "", - "info": "" + "label": "Laika zona", + "info": "Izvēlieties savas laika zonas nosaukumu, atrodiet savējo Å”eit: " }, "customTitle": { - "label": "" + "label": "Pilsētas nosaukums vai pielāgots nosaukums" }, "display24HourFormat": { "label": "RādÄ«t pilnu laiku (24 stundu)" @@ -21,11 +21,11 @@ } }, "titleState": { - "label": "", - "info": "", + "label": "Pulksteņa nosaukums", + "info": "Pielāgotais nosaukums un laika zonas kods var tikt parādÄ«ts jÅ«su logrÄ«kā.
Varat arī rādīt tikai pilsētu, nerādīt nevienu,
vai pat rādÄ«t tikai laika joslu, gadÄ«jumā ja ir atlasÄ«ti abi, bet nav norādÄ«ts nosaukums.", "data": { - "both": "", - "city": "", + "both": "Pilsēta un Laika zona", + "city": "Tikai nosaukums", "none": "Nekas" } } diff --git a/public/locales/lv/modules/rss.json b/public/locales/lv/modules/rss.json index c37d5e1327e..493eec21730 100644 --- a/public/locales/lv/modules/rss.json +++ b/public/locales/lv/modules/rss.json @@ -19,13 +19,13 @@ "label": "Teksta lÄ«niju skava" }, "sortByPublishDateAscending": { - "label": "" + "label": "Kārtot pēc publicÄ“Å”anas datuma (augoŔā secÄ«bā)" }, "sortPostsWithoutPublishDateToTheTop": { - "label": "" + "label": "Ievietot ziņas bez publicÄ“Å”anas datuma augÅ”pusē" }, "maximumAmountOfPosts": { - "label": "" + "label": "Maksimālais ierakstu skaits" } }, "card": { diff --git a/public/locales/lv/modules/smart-home/entity-state.json b/public/locales/lv/modules/smart-home/entity-state.json index 0ce4fcc65c6..f3e41152f44 100644 --- a/public/locales/lv/modules/smart-home/entity-state.json +++ b/public/locales/lv/modules/smart-home/entity-state.json @@ -1,20 +1,20 @@ { - "entityNotFound": "", + "entityNotFound": "VienÄ«ba nav atrasta", "descriptor": { - "name": "", - "description": "", + "name": "Home Assistant vienÄ«ba", + "description": "VienÄ«bas paÅ”reizējais stāvoklis pakalpojumā Home Assistant", "settings": { - "title": "", + "title": "VienÄ«bas stāvoklis", "entityId": { - "label": "", - "info": "" + "label": "VienÄ«bas ID", + "info": "Unikāls vienÄ«bas ID pakalpojumā Home Assistant. Ievietojiet starpliktuvē, noklikŔķinot uz vienÄ«bu > NoklikŔķiniet uz zobrata ikonu > NoklikŔķiniet uz kopÄ“Å”anas pogu pie \"VienÄ«bas ID\". Dažas pielāgotas vienÄ«bas var nebÅ«t atbalstÄ«tas." }, "automationId": { - "label": "", - "info": "" + "label": "Izvēles automatizācijasĀ ID", + "info": "JÅ«su unikālais automatizācijasĀ ID. Vienmēr sākas ar automatizāciju.XXXX. Ja tas nav iestatÄ«ts, logrÄ«ks nebÅ«s noklikŔķināms un tiks parādÄ«ts tikai statuss. Pēc noklikŔķināŔanas vienÄ«bas stāvoklis tiks atsvaidzināts." }, "displayName": { - "label": "" + "label": "Parādāmais nosaukums" } } } diff --git a/public/locales/lv/modules/smart-home/trigger-automation.json b/public/locales/lv/modules/smart-home/trigger-automation.json index 37046b5cf74..333f3b542ae 100644 --- a/public/locales/lv/modules/smart-home/trigger-automation.json +++ b/public/locales/lv/modules/smart-home/trigger-automation.json @@ -1,15 +1,15 @@ { "descriptor": { - "name": "", - "description": "", + "name": "Home Assistant automatizācija", + "description": "Automatizācijas izpilde", "settings": { - "title": "", + "title": "Automatizācijas izpilde", "automationId": { - "label": "", - "info": "" + "label": "Automatizācijas ID", + "info": "JÅ«su unikālais automatizācijas ID. Vienmēr sāksies ar automation.XXXXX." }, "displayName": { - "label": "" + "label": "Parādāmais nosaukums" } } } diff --git a/public/locales/lv/modules/torrents-status.json b/public/locales/lv/modules/torrents-status.json index 84beeea59d5..5f8e671d56f 100644 --- a/public/locales/lv/modules/torrents-status.json +++ b/public/locales/lv/modules/torrents-status.json @@ -41,22 +41,22 @@ }, "table": { "header": { - "isCompleted": "", + "isCompleted": "Lejupielādē", "name": "Nosaukums", - "dateAdded": "", + "dateAdded": "Pievienots", "size": "Lielums", "download": "Lejupielāde", "upload": "AugÅ”upielāde", "estimatedTimeOfArrival": "ETA", "progress": "Progress", - "totalUploaded": "", - "totalDownloaded": "", - "ratio": "", - "seeds": "", - "peers": "", - "label": "", + "totalUploaded": "Kopējā AugÅ”upielāde", + "totalDownloaded": "Kopējā Lejupielāde", + "ratio": "AttiecÄ«ba", + "seeds": "Devēji (savienoti)", + "peers": "Ņēmēji (savienoti)", + "label": "Birka", "state": "Stāvoklis", - "stateMessage": "" + "stateMessage": "Statusa Ziņojums" }, "item": { "text": "Pārvalda {{appName}}, {{ratio}} attiecÄ«ba" diff --git a/public/locales/lv/settings/customization/page-appearance.json b/public/locales/lv/settings/customization/page-appearance.json index cfbb998c90e..ca39431c93b 100644 --- a/public/locales/lv/settings/customization/page-appearance.json +++ b/public/locales/lv/settings/customization/page-appearance.json @@ -19,26 +19,26 @@ "label": "Fons" }, "backgroundImageAttachment": { - "label": "", + "label": "Fona attēla pielikums", "options": { - "fixed": "", - "scroll": "" + "fixed": "Fiksēts - fons paliek nemainÄ«gā pozÄ«cijā (ieteicams)", + "scroll": "RitināŔana - fons ritinās lÄ«dz ar kursora ritināŔanu" } }, "backgroundImageSize": { - "label": "", + "label": "Fona attēla izmērs", "options": { - "cover": "", - "contain": "" + "cover": "Pārklājums - pēc iespējas mazāks attēla mērogs, lai, apgriežot lieko vietu, pārklātu visu logu. (ieteicams)", + "contain": "Saturēt ā€” mērogo attēlu pēc iespējas lielāku tā konteinerā, neapgriežot vai neizstiepjot attēlu." } }, "backgroundImageRepeat": { - "label": "", + "label": "Fona attēla pielikums", "options": { - "repeat": "", - "no-repeat": "", - "repeat-x": "", - "repeat-y": "" + "repeat": "Atkārtot ā€” attēls tiek atkārtots tik daudz, cik nepiecieÅ”ams, lai aptvertu visu fona laukumu.", + "no-repeat": "Bez atkārtojuma - attēls neatkārtojas un var neaizpildÄ«t visu fona laukumu (ieteicams)", + "repeat-x": "Atkārtot X - tāpat kā \"Atkārtot\", bet tikai uz horizontālās ass.", + "repeat-y": "Atkārtot Y - tāpat kā \"Atkārtot\", bet tikai uz vertikālās ass." } }, "customCSS": { diff --git a/public/locales/lv/tools/docker.json b/public/locales/lv/tools/docker.json index 453131d1e64..e9f90563f7a 100644 --- a/public/locales/lv/tools/docker.json +++ b/public/locales/lv/tools/docker.json @@ -2,7 +2,7 @@ "title": "Docker", "alerts": { "notConfigured": { - "text": "" + "text": "JÅ«su Homarr instancē nav konfigurēts Docker vai arÄ« nav izdevies iegÅ«tu konteinerus. LÅ«dzu, pārbaudiet dokumentāciju par to, kā iestatÄ«t integrāciju." } }, "modals": { diff --git a/public/locales/sk/modules/rss.json b/public/locales/sk/modules/rss.json index 9045676f29c..bf3b6ef3696 100644 --- a/public/locales/sk/modules/rss.json +++ b/public/locales/sk/modules/rss.json @@ -22,10 +22,10 @@ "label": "ZoradiÅ„ podľa dĆ”tumu vydania (vzostupne)" }, "sortPostsWithoutPublishDateToTheTop": { - "label": "" + "label": "Umiestnite prĆ­spevky bez dĆ”tumu uverejnenia na začiatok" }, "maximumAmountOfPosts": { - "label": "" + "label": "MaximĆ”lny počet prĆ­spevkov" } }, "card": { diff --git a/public/locales/tr/common.json b/public/locales/tr/common.json index eecc4502130..4ed9134f62d 100644 --- a/public/locales/tr/common.json +++ b/public/locales/tr/common.json @@ -17,8 +17,8 @@ "disabled": "Pasif", "enableAll": "TĆ¼mĆ¼nĆ¼ etkinleştir", "disableAll": "TĆ¼mĆ¼nĆ¼ pasifleştir", - "version": "Versiyon", - "changePosition": "Pozisyon değiştir", + "version": "SĆ¼rĆ¼m", + "changePosition": "Pozisyonu değiştir", "remove": "Kaldır", "removeConfirm": "{{item}}'i kaldırmak istediğinizden emin misiniz?", "createItem": "+ yeni {{item}}", diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx index eb021a94c88..21ad225009c 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector.tsx @@ -163,6 +163,11 @@ export const availableIntegrations = [ image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png', label: 'Readarr', }, + { + value: 'prowlarr', + image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/prowlarr.png', + label: 'Prowlarr', + }, { value: 'jellyfin', image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png', @@ -186,6 +191,6 @@ export const availableIntegrations = [ { value: 'homeAssistant', image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png', - label: 'Home Assistant' - } + label: 'Home Assistant', + }, ] as const satisfies Readonly; diff --git a/src/components/Manage/Board/create-board.modal.tsx b/src/components/Manage/Board/create-board.modal.tsx index 7cf7abe4e66..0f440f0bc96 100644 --- a/src/components/Manage/Board/create-board.modal.tsx +++ b/src/components/Manage/Board/create-board.modal.tsx @@ -9,12 +9,15 @@ import { createBoardSchemaValidation } from '~/validations/boards'; export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => { const { t } = useTranslation('manage/boards'); - const utils = api.useContext(); + const utils = api.useUtils(); const { isLoading, mutate } = api.config.save.useMutation({ onSuccess: async () => { await utils.boards.all.invalidate(); modals.close(id); }, + onError: async (error) => { + form.setFieldError('name', error.message); + }, }); const { i18nZodResolver } = useI18nZodResolver(); @@ -31,6 +34,7 @@ export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => { mutate({ name: form.values.name, config: fallbackConfig, + create: true, }); }; @@ -59,7 +63,7 @@ export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => { + )} + + {providers.includes('ldap') && ( + + )} + + {redirectAfterLogin && ( + + {t('form.afterLoginRedirection', { url: redirectAfterLogin })} + + )} + + + )} + {hasCredentialsInput && providers.includes('oidc') && ( + + )} + {providers.includes('oidc') && ( + - - {redirectAfterLogin && ( - - {t('form.afterLoginRedirection', { url: redirectAfterLogin })} - - )} - - - + )} + + )} @@ -136,7 +200,12 @@ export default function LoginPage({ const regexExp = /^\/{1}[A-Za-z\/]*$/; -export const getServerSideProps: GetServerSideProps = async ({ locale, req, res, query }) => { +export const getServerSideProps = async ({ + locale, + req, + res, + query, +}: GetServerSidePropsContext) => { const session = await getServerAuthSession({ req, res }); const zodResult = await z @@ -159,6 +228,9 @@ export const getServerSideProps: GetServerSideProps = async ({ locale, req, res, props: { ...(await getServerSideTranslations(['authentication/login'], locale, req, res)), redirectAfterLogin, + providers: env.AUTH_PROVIDER, + oidcProviderName: env.AUTH_OIDC_CLIENT_NAME || null, + oidcAutoLogin: env.AUTH_OIDC_AUTO_LOGIN || null, isDemo, }, }; diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 2b34e1b3e2e..0d12680791c 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,4 +1,5 @@ import { createTRPCRouter } from '~/server/api/trpc'; + import { appRouter } from './routers/app'; import { boardRouter } from './routers/board'; import { calendarRouter } from './routers/calendar'; @@ -8,6 +9,7 @@ import { dnsHoleRouter } from './routers/dns-hole/router'; import { dockerRouter } from './routers/docker/router'; import { downloadRouter } from './routers/download'; import { iconRouter } from './routers/icon'; +import { indexerManagerRouter } from './routers/indexer-manager'; import { inviteRouter } from './routers/invite/invite-router'; import { mediaRequestsRouter } from './routers/media-request'; import { mediaServerRouter } from './routers/media-server'; @@ -30,6 +32,7 @@ export const rootRouter = createTRPCRouter({ rss: rssRouter, user: userRouter, calendar: calendarRouter, + indexerManager: indexerManagerRouter, config: configRouter, dashDot: dashDotRouter, dnsHole: dnsHoleRouter, @@ -45,7 +48,7 @@ export const rootRouter = createTRPCRouter({ boards: boardRouter, password: passwordRouter, notebook: notebookRouter, - smartHomeEntityState: smartHomeEntityStateRouter + smartHomeEntityState: smartHomeEntityStateRouter, }); // export type definition of API diff --git a/src/server/api/routers/config.ts b/src/server/api/routers/config.ts index 10f15912cd1..6344a70ae2a 100644 --- a/src/server/api/routers/config.ts +++ b/src/server/api/routers/config.ts @@ -19,15 +19,15 @@ export const configRouter = createTRPCRouter({ .input( z.object({ name: configNameSchema, - }), + }) ) .output(z.object({ message: z.string() })) .mutation(async ({ input }) => { if (input.name.toLowerCase() === 'default') { - Consola.error('Rejected config deletion because default configuration can\'t be deleted'); + Consola.error("Rejected config deletion because default configuration can't be deleted"); throw new TRPCError({ code: 'FORBIDDEN', - message: 'Default config can\'t be deleted', + message: "Default config can't be deleted", }); } @@ -44,7 +44,7 @@ export const configRouter = createTRPCRouter({ // If the target is not in the list of files, return an error if (!matchedFile) { Consola.error( - `Rejected config deletion request because config name '${input.name}' was not included in present configurations`, + `Rejected config deletion request because config name '${input.name}' was not included in present configurations` ); throw new TRPCError({ code: 'NOT_FOUND', @@ -64,9 +64,13 @@ export const configRouter = createTRPCRouter({ z.object({ name: configNameSchema, config: z.custom((x) => !!x && typeof x === 'object'), - }), + create: z.boolean().optional(), + }) ) .mutation(async ({ input }) => { + if (input.create && configExists(input.name)) + throw new TRPCError({ message: 'Config already exists.', code: 'CONFLICT' }); + Consola.info(`Saving updated configuration of '${input.name}' config.`); const previousConfig = getConfig(input.name); @@ -96,16 +100,16 @@ export const configRouter = createTRPCRouter({ } const previousApp = previousConfig.apps.find( - (previousApp) => previousApp.id === app.id, + (previousApp) => previousApp.id === app.id ); const previousProperty = previousApp?.integration?.properties.find( - (previousProperty) => previousProperty.field === property.field, + (previousProperty) => previousProperty.field === property.field ); if (property.value !== undefined && property.value !== null) { Consola.info( - 'Detected credential change of private secret. Value will be overwritten in configuration', + 'Detected credential change of private secret. Value will be overwritten in configuration' ); return { field: property.field, @@ -168,13 +172,14 @@ export const configRouter = createTRPCRouter({ path: '/configs/byName', tags: ['config'], deprecated: true, - summary: 'Retrieve content of the JSON configuration. Deprecated because JSON will be removed in a future version and be replaced with a relational database.' - } + summary: + 'Retrieve content of the JSON configuration. Deprecated because JSON will be removed in a future version and be replaced with a relational database.', + }, }) .input( z.object({ name: configNameSchema, - }), + }) ) .output(z.custom()) .query(async ({ ctx, input }) => { diff --git a/src/server/api/routers/indexer-manager.ts b/src/server/api/routers/indexer-manager.ts new file mode 100644 index 00000000000..2fc43b2f5c5 --- /dev/null +++ b/src/server/api/routers/indexer-manager.ts @@ -0,0 +1,102 @@ +import axios from 'axios'; +import Consola from 'consola'; +import { z } from 'zod'; +import { checkIntegrationsType, findAppProperty } from '~/tools/client/app-properties'; +import { getConfig } from '~/tools/config/getConfig'; +import { IntegrationType } from '~/types/app'; + +import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; + +export const indexerManagerRouter = createTRPCRouter({ + indexers: publicProcedure + .input( + z.object({ + configName: z.string(), + }) + ) + .query(async ({ input }) => { + const config = getConfig(input.configName); + const indexerAppIntegrationTypes = ['prowlarr'] as const satisfies readonly IntegrationType[]; + const app = config.apps.find((app) => + checkIntegrationsType(app.integration, indexerAppIntegrationTypes) + )!; + const apiKey = findAppProperty(app, 'apiKey'); + if (!app || !apiKey) { + Consola.error( + `Failed to process request to indexer app (${app.id}): API key not found. Please check the configuration.` + ); + } + + const appUrl = new URL(app.url); + const data = await axios + .get(`${appUrl.origin}/api/v1/indexer`, { + headers: { + 'X-Api-Key': apiKey, + }, + }) + .then((res) => res.data); + return data; + }), + + statuses: publicProcedure + .input( + z.object({ + configName: z.string(), + }) + ) + .query(async ({ input }) => { + const config = getConfig(input.configName); + const indexerAppIntegrationTypes = ['prowlarr'] as const satisfies readonly IntegrationType[]; + const app = config.apps.find((app) => + checkIntegrationsType(app.integration, indexerAppIntegrationTypes) + )!; + const apiKey = findAppProperty(app, 'apiKey'); + if (!app || !apiKey) { + Consola.error( + `Failed to process request to indexer app (${app.id}): API key not found. Please check the configuration.` + ); + } + + const appUrl = new URL(app.url); + const data = await axios + .get(`${appUrl.origin}/api/v1/indexerstatus`, { + headers: { + 'X-Api-Key': apiKey, + }, + }) + .then((res) => res.data); + return data; + }), + + testAllIndexers: protectedProcedure + .input( + z.object({ + configName: z.string(), + }) + ) + .mutation(async ({ input }) => { + const config = getConfig(input.configName); + const indexerAppIntegrationTypes = ['prowlarr'] as const satisfies readonly IntegrationType[]; + const app = config.apps.find((app) => + checkIntegrationsType(app.integration, indexerAppIntegrationTypes) + )!; + const apiKey = findAppProperty(app, 'apiKey'); + if (!app || !apiKey) { + Consola.error( + `failed to process request to app '${app?.integration}' (${app?.id}). Please check api key` + ); + } + + const appUrl = new URL(app.url); + const result = await axios + .post(`${appUrl.origin}/api/v1/indexer/testall`, null, { + headers: { + 'X-Api-Key': apiKey, + }, + }) + .then((res) => res.data) + .catch((err: any) => err.response.data); + + return result; + }), +}); diff --git a/src/server/auth.ts b/src/server/auth.ts index 5d79c62f5dd..46ea2f539d3 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -1,57 +1,17 @@ -import { DrizzleAdapter } from '@auth/drizzle-adapter'; -import bcrypt from 'bcryptjs'; -import Consola from 'consola'; import Cookies from 'cookies'; import { eq } from 'drizzle-orm'; import { type GetServerSidePropsContext, type NextApiRequest, type NextApiResponse } from 'next'; -import { type DefaultSession, type NextAuthOptions, getServerSession } from 'next-auth'; +import { type NextAuthOptions, getServerSession } from 'next-auth'; import { Adapter } from 'next-auth/adapters'; import { decode, encode } from 'next-auth/jwt'; -import Credentials from 'next-auth/providers/credentials'; +import { adapter, onCreateUser, providers } from '~/utils/auth'; import EmptyNextAuthProvider from '~/utils/empty-provider'; import { fromDate, generateSessionToken } from '~/utils/session'; -import { colorSchemeParser, signInSchema } from '~/validations/user'; +import { colorSchemeParser } from '~/validations/user'; import { db } from './db'; import { users } from './db/schema'; -/** - * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` - * object and keep type safety. - * - * @see https://next-auth.js.org/getting-started/typescript#module-augmentation - */ -declare module 'next-auth' { - interface Session extends DefaultSession { - user: DefaultSession['user'] & { - id: string; - isAdmin: boolean; - colorScheme: 'light' | 'dark' | 'environment'; - autoFocusSearch: boolean; - language: string; - // ...other properties - // role: UserRole; - }; - } - - interface User { - isAdmin: boolean; - colorScheme: 'light' | 'dark' | 'environment'; - autoFocusSearch: boolean; - language: string; - // ...other properties - // role: UserRole; - } -} - -declare module 'next-auth/jwt' { - interface JWT { - id: string; - isAdmin: boolean; - } -} - -const adapter = DrizzleAdapter(db); const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days /** @@ -63,6 +23,9 @@ export const constructAuthOptions = ( req: NextApiRequest, res: NextApiResponse ): NextAuthOptions => ({ + events: { + createUser: onCreateUser, + }, callbacks: { async session({ session, user }) { if (session.user) { @@ -133,58 +96,7 @@ export const constructAuthOptions = ( error: '/auth/login', }, adapter: adapter as Adapter, - providers: [ - Credentials({ - name: 'credentials', - credentials: { - name: { - label: 'Username', - type: 'text', - }, - password: { label: 'Password', type: 'password' }, - }, - async authorize(credentials) { - const data = await signInSchema.parseAsync(credentials); - - const user = await db.query.users.findFirst({ - with: { - settings: { - columns: { - colorScheme: true, - language: true, - autoFocusSearch: true, - }, - }, - }, - where: eq(users.name, data.name), - }); - - if (!user || !user.password) { - return null; - } - - Consola.log(`user ${user.name} is trying to log in. checking password...`); - const isValidPassword = await bcrypt.compare(data.password, user.password); - - if (!isValidPassword) { - Consola.log(`password for user ${user.name} was incorrect`); - return null; - } - - Consola.log(`user ${user.name} successfully authorized`); - - return { - id: user.id, - name: user.name, - isAdmin: false, - colorScheme: colorSchemeParser.parse(user.settings?.colorScheme), - language: user.settings?.language ?? 'en', - autoFocusSearch: user.settings?.autoFocusSearch ?? false, - }; - }, - }), - EmptyNextAuthProvider(), - ], + providers: [...providers, EmptyNextAuthProvider()], jwt: { async encode(params) { if (!isCredentialsRequest(req)) { @@ -207,10 +119,12 @@ export const constructAuthOptions = ( }); const isCredentialsRequest = (req: NextApiRequest): boolean => { - const nextAuthQueryParams = req.query.nextauth as ['callback', 'credentials']; + const nextAuthQueryParams = req.query.nextauth as string[]; return ( nextAuthQueryParams.includes('callback') && - nextAuthQueryParams.includes('credentials') && + (nextAuthQueryParams.includes('credentials') || + nextAuthQueryParams.includes('ldap') || + nextAuthQueryParams.includes('oidc')) && req.method === 'POST' ); }; diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 470f96a5ee2..9ee168d0599 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -2,7 +2,9 @@ import { InferSelectModel, relations } from 'drizzle-orm'; import { index, int, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { type AdapterAccount } from 'next-auth/adapters'; -export const users = sqliteTable('user', { +// workaround for typescript check in adapter +// preferably add email into credential login and make email non-nullable here +export const _users = { id: text('id').notNull().primaryKey(), name: text('name'), email: text('email'), @@ -12,7 +14,9 @@ export const users = sqliteTable('user', { salt: text('salt'), isAdmin: int('is_admin', { mode: 'boolean' }).notNull().default(false), isOwner: int('is_owner', { mode: 'boolean' }).notNull().default(false), -}); +}; + +export const users = sqliteTable('user', _users); export const accounts = sqliteTable( 'account', diff --git a/src/tools/server/translation-namespaces.ts b/src/tools/server/translation-namespaces.ts index e505ba8a322..33487f83d95 100644 --- a/src/tools/server/translation-namespaces.ts +++ b/src/tools/server/translation-namespaces.ts @@ -22,6 +22,7 @@ export const boardNamespaces = [ 'modules/dashdot', 'modules/overseerr', 'modules/media-server', + 'modules/indexer-manager', 'modules/common-media-cards', 'modules/video-stream', 'modules/media-requests-list', @@ -44,7 +45,7 @@ export const manageNamespaces = [ 'manage/users', 'manage/users/invites', 'manage/users/create', - 'manage/users/edit' + 'manage/users/edit', ]; export const loginNamespaces = ['authentication/login']; diff --git a/src/types/app.ts b/src/types/app.ts index 6c624ca5d8b..19258278cd4 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -1,5 +1,4 @@ import { Icon, IconKey, IconPassword, IconUser } from '@tabler/icons-react'; - import { Property } from 'csstype'; import { TileBaseType } from './tile'; @@ -46,6 +45,7 @@ export type IntegrationType = | 'radarr' | 'sonarr' | 'lidarr' + | 'prowlarr' | 'sabnzbd' | 'jellyseerr' | 'overseerr' @@ -87,6 +87,7 @@ export const integrationFieldProperties: { lidarr: ['apiKey'], radarr: ['apiKey'], sonarr: ['apiKey'], + prowlarr: ['apiKey'], sabnzbd: ['apiKey'], readarr: ['apiKey'], overseerr: ['apiKey'], @@ -99,7 +100,7 @@ export const integrationFieldProperties: { plex: ['apiKey'], pihole: ['apiKey'], adGuardHome: ['username', 'password'], - homeAssistant: ['apiKey'] + homeAssistant: ['apiKey'], }; export type IntegrationFieldDefinitionType = { diff --git a/src/utils/auth/adapter.ts b/src/utils/auth/adapter.ts new file mode 100644 index 00000000000..7fe34f184c8 --- /dev/null +++ b/src/utils/auth/adapter.ts @@ -0,0 +1,166 @@ +import { randomUUID } from 'crypto'; +import { and, eq } from 'drizzle-orm'; +import { + BaseSQLiteDatabase, + SQLiteTableFn, + sqliteTable as defaultSqliteTableFn, + text, +} from 'drizzle-orm/sqlite-core'; +import { User } from 'next-auth'; +import { Adapter, AdapterAccount } from 'next-auth/adapters'; +import { db } from '~/server/db'; +import { _users, accounts, sessions, userSettings, verificationTokens } from '~/server/db/schema'; + +// Need to modify createTables with custom schema +const createTables = (sqliteTable: SQLiteTableFn) => ({ + users: sqliteTable('user', { + ..._users, + email: text('email').notNull(), // workaround for typescript + }), + accounts, + sessions, + verificationTokens, +}); + +export type DefaultSchema = ReturnType; + +export const onCreateUser = async ({ user }: { user: User }) => { + await db.insert(userSettings).values({ + id: randomUUID(), + userId: user.id, + }); +}; + +// Keep this the same as original file @auth/drizzle-adapter/src/lib/sqlite.ts +// only change changed return type from Adapter to "satisfies Adapter", to tell typescript createUser exists + +export function SQLiteDrizzleAdapter( + client: InstanceType, + tableFn = defaultSqliteTableFn +) { + const { users, accounts, sessions, verificationTokens } = createTables(tableFn); + + return { + createUser(data) { + return client + .insert(users) + .values({ ...data, id: crypto.randomUUID() }) + .returning() + .get(); + }, + getUser(data) { + return client.select().from(users).where(eq(users.id, data)).get() ?? null; + }, + getUserByEmail(data) { + return client.select().from(users).where(eq(users.email, data)).get() ?? null; + }, + createSession(data) { + return client.insert(sessions).values(data).returning().get(); + }, + getSessionAndUser(data) { + return ( + client + .select({ + session: sessions, + user: users, + }) + .from(sessions) + .where(eq(sessions.sessionToken, data)) + .innerJoin(users, eq(users.id, sessions.userId)) + .get() ?? null + ); + }, + updateUser(data) { + if (!data.id) { + throw new Error('No user id.'); + } + + return client.update(users).set(data).where(eq(users.id, data.id)).returning().get(); + }, + updateSession(data) { + return client + .update(sessions) + .set(data) + .where(eq(sessions.sessionToken, data.sessionToken)) + .returning() + .get(); + }, + linkAccount(rawAccount) { + const updatedAccount = client.insert(accounts).values(rawAccount).returning().get(); + + const account: AdapterAccount = { + ...updatedAccount, + type: updatedAccount.type, + access_token: updatedAccount.access_token ?? undefined, + token_type: updatedAccount.token_type ?? undefined, + id_token: updatedAccount.id_token ?? undefined, + refresh_token: updatedAccount.refresh_token ?? undefined, + scope: updatedAccount.scope ?? undefined, + expires_at: updatedAccount.expires_at ?? undefined, + session_state: updatedAccount.session_state ?? undefined, + }; + + return account; + }, + getUserByAccount(account) { + const results = client + .select() + .from(accounts) + .leftJoin(users, eq(users.id, accounts.userId)) + .where( + and( + eq(accounts.provider, account.provider), + eq(accounts.providerAccountId, account.providerAccountId) + ) + ) + .get(); + + return results?.user ?? null; + }, + deleteSession(sessionToken) { + return ( + client.delete(sessions).where(eq(sessions.sessionToken, sessionToken)).returning().get() ?? + null + ); + }, + createVerificationToken(token) { + return client.insert(verificationTokens).values(token).returning().get(); + }, + useVerificationToken(token) { + try { + return ( + client + .delete(verificationTokens) + .where( + and( + eq(verificationTokens.identifier, token.identifier), + eq(verificationTokens.token, token.token) + ) + ) + .returning() + .get() ?? null + ); + } catch (err) { + throw new Error('No verification token found.'); + } + }, + deleteUser(id) { + return client.delete(users).where(eq(users.id, id)).returning().get(); + }, + unlinkAccount(account) { + client + .delete(accounts) + .where( + and( + eq(accounts.providerAccountId, account.providerAccountId), + eq(accounts.provider, account.provider) + ) + ) + .run(); + + return undefined; + }, + } satisfies Adapter; +} + +export default SQLiteDrizzleAdapter(db); diff --git a/src/utils/auth/credentials.ts b/src/utils/auth/credentials.ts new file mode 100644 index 00000000000..199c1e23978 --- /dev/null +++ b/src/utils/auth/credentials.ts @@ -0,0 +1,56 @@ +import bcrypt from 'bcryptjs'; +import Consola from 'consola'; +import { eq } from 'drizzle-orm'; +import Credentials from 'next-auth/providers/credentials'; +import { colorSchemeParser, signInSchema } from '~/validations/user'; + +import { db } from '../../server/db'; +import { users } from '../../server/db/schema'; + +export default Credentials({ + name: 'credentials', + credentials: { + name: { + label: 'Username', + type: 'text', + }, + password: { label: 'Password', type: 'password' }, + }, + async authorize(credentials) { + const data = await signInSchema.parseAsync(credentials); + + const user = await db.query.users.findFirst({ + with: { + settings: { + columns: { + colorScheme: true, + language: true, + autoFocusSearch: true, + }, + }, + }, + where: eq(users.name, data.name), + }); + + if (!user || !user.password) { + return null; + } + + Consola.log(`user ${user.name} is trying to log in. checking password...`); + const isValidPassword = await bcrypt.compare(data.password, user.password); + + if (!isValidPassword) { + Consola.log(`password for user ${user.name} was incorrect`); + return null; + } + + Consola.log(`user ${user.name} successfully authorized`); + + return { + id: user.id, + name: user.name, + isAdmin: false, + isOwner: false, + }; + }, +}); diff --git a/src/utils/auth/index.ts b/src/utils/auth/index.ts new file mode 100644 index 00000000000..73c7ea5db30 --- /dev/null +++ b/src/utils/auth/index.ts @@ -0,0 +1,46 @@ +import { DefaultSession } from 'next-auth'; +import { CredentialsConfig, OAuthConfig } from 'next-auth/providers'; +import { env } from '~/env'; + +export { default as adapter, onCreateUser } from './adapter'; + +/** + * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` + * object and keep type safety. + * + * @see https://next-auth.js.org/getting-started/typescript#module-augmentation + */ +declare module 'next-auth' { + interface Session extends DefaultSession { + user: DefaultSession['user'] & { + id: string; + isAdmin: boolean; + colorScheme: 'light' | 'dark' | 'environment'; + autoFocusSearch: boolean; + language: string; + // ...other properties + // role: UserRole; + }; + } + + interface User { + isAdmin: boolean; + isOwner?: boolean; + // ...other properties + // role: UserRole; + } +} + +declare module 'next-auth/jwt' { + interface JWT { + id: string; + isAdmin: boolean; + } +} + +export const providers: (CredentialsConfig | OAuthConfig)[] = []; + +if (env.AUTH_PROVIDER?.includes('ldap')) providers.push((await import('./ldap')).default); +if (env.AUTH_PROVIDER?.includes('credentials')) + providers.push((await import('./credentials')).default); +if (env.AUTH_PROVIDER?.includes('oidc')) providers.push((await import('./oidc')).default); diff --git a/src/utils/auth/ldap.ts b/src/utils/auth/ldap.ts new file mode 100644 index 00000000000..91d0761b1e6 --- /dev/null +++ b/src/utils/auth/ldap.ts @@ -0,0 +1,161 @@ +import Consola from 'consola'; +import ldap from 'ldapjs'; +import Credentials from 'next-auth/providers/credentials'; +import { env } from '~/env'; +import { signInSchema } from '~/validations/user'; + +import adapter, { onCreateUser } from './adapter'; + +// Helper types for infering properties of returned search type +type AttributeConstraint = string | readonly string[] | undefined; + +type InferrableSearchOptions< + Attributes extends AttributeConstraint, + ArrayAttributes extends Attributes, +> = Omit & { + attributes?: Attributes; + arrayAttributes?: ArrayAttributes; +}; + +type SearchResultIndex = Attributes extends string + ? Attributes + : Attributes extends readonly string[] + ? Attributes[number] + : string; + +type SearchResult< + Attributes extends AttributeConstraint, + ArrayAttributes extends Attributes = never, +> = { dn: string } & Record< + Exclude, SearchResultIndex>, + string +> & + Record, string[]>; + +const ldapLogin = (username: string, password: string) => + new Promise((resolve, reject) => { + const client = ldap.createClient({ + url: env.AUTH_LDAP_URI, + }); + client.bind(username, password, (error, res) => { + if (error) { + reject('Invalid username or password'); + } else { + resolve(client); + } + }); + }); + +const ldapSearch = async < + Attributes extends AttributeConstraint, + ArrayAttributes extends Attributes = never, +>( + client: ldap.Client, + base: string, + options: InferrableSearchOptions +) => + new Promise[]>((resolve, reject) => { + client.search(base, options as ldap.SearchOptions, (err, res) => { + const results: SearchResult[] = []; + res.on('error', (err) => { + reject('error: ' + err.message); + }); + res.on('searchEntry', (entry) => { + results.push( + entry.pojo.attributes.reduce>( + (obj, attr) => { + // just take first element assuming there's only one (uid, mail), unless in arrayAttributes + obj[attr.type] = options.arrayAttributes?.includes(attr.type) + ? attr.values + : attr.values[0]; + return obj; + }, + { dn: entry.pojo.objectName } + ) as SearchResult + ); + }); + res.on('end', (result) => { + if (result?.status != 0) { + reject(new Error('ldap search status is not 0, search failed')); + } else { + resolve(results); + } + }); + }); + }); + +export default Credentials({ + id: 'ldap', + name: 'LDAP', + credentials: { + name: { label: 'uid', type: 'text' }, + password: { label: 'Password', type: 'password' }, + }, + async authorize(credentials) { + try { + const data = await signInSchema.parseAsync(credentials); + + Consola.log(`user ${data.name} is trying to log in using LDAP. Signing in...`); + const client = await ldapLogin(env.AUTH_LDAP_BIND_DN, env.AUTH_LDAP_BIND_PASSWORD); + + const ldapUser = ( + await ldapSearch(client, env.AUTH_LDAP_BASE, { + filter: `(uid=${data.name})`, + // as const for inference + attributes: ['uid', 'mail'] as const, + }) + )[0]; + + await ldapLogin(ldapUser.dn, data.password).then((client) => client.destroy()); + + const userGroups = ( + await ldapSearch(client, env.AUTH_LDAP_BASE, { + filter: `(&(objectclass=${env.AUTH_LDAP_GROUP_CLASS})(${ + env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE + }=${ldapUser[env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE as 'dn' | 'uid']}))`, + // as const for inference + attributes: 'cn', + }) + ).map((group) => group.cn); + + client.destroy(); + + Consola.log(`user ${data.name} successfully authorized`); + + let user = await adapter.getUserByEmail!(ldapUser.mail); + const isAdmin = userGroups.includes(env.AUTH_LDAP_ADMIN_GROUP); + const isOwner = userGroups.includes(env.AUTH_LDAP_OWNER_GROUP); + + if (!user) { + // CreateUser will create settings in event + user = await adapter.createUser({ + name: ldapUser.uid, + email: ldapUser.mail, + emailVerified: new Date(), // assume ldap email is verified + isAdmin: isAdmin, + isOwner: isOwner, + }); + // For some reason adapter.createUser doesn't call createUser event, needs to be called manually to create usersettings + await onCreateUser({ user }); + } else if (user.isAdmin != isAdmin || user.isOwner != isOwner) { + // Update roles if changed in LDAP + Consola.log(`updating roles of user ${user.name}`); + adapter.updateUser({ + ...user, + isAdmin, + isOwner, + }); + } + + return { + id: user?.id || ldapUser.dn, + name: user?.name || ldapUser.uid, + isAdmin: isAdmin, + isOwner: isOwner, + }; + } catch (error) { + Consola.error(error); + return null; + } + }, +}); diff --git a/src/utils/auth/oidc.ts b/src/utils/auth/oidc.ts new file mode 100644 index 00000000000..8d9c3ef7643 --- /dev/null +++ b/src/utils/auth/oidc.ts @@ -0,0 +1,51 @@ +import Consola from 'consola'; +import { OAuthConfig } from 'next-auth/providers/oauth'; +import { env } from '~/env'; + +import adapter from './adapter'; + +type Profile = { + sub: string; + name: string; + email: string; + groups: string[]; + preferred_username: string; + email_verified: boolean; +}; + +const provider: OAuthConfig = { + id: 'oidc', + name: env.AUTH_OIDC_CLIENT_NAME, + type: 'oauth', + clientId: env.AUTH_OIDC_CLIENT_ID, + clientSecret: env.AUTH_OIDC_CLIENT_SECRET, + wellKnown: `${env.AUTH_OIDC_URI}/.well-known/openid-configuration`, + authorization: { params: { scope: 'openid email profile groups' } }, + idToken: true, + async profile(profile) { + const user = await adapter.getUserByEmail!(profile.email); + + const isAdmin = profile.groups.includes(env.AUTH_OIDC_ADMIN_GROUP); + const isOwner = profile.groups.includes(env.AUTH_OIDC_OWNER_GROUP); + + // check for role update + if (user && (user.isAdmin != isAdmin || user.isOwner != isOwner)) { + Consola.log(`updating roles of user ${user.name}`); + adapter.updateUser({ + ...user, + isAdmin, + isOwner, + }); + } + + return { + id: profile.sub, + name: profile.preferred_username, + email: profile.email, + isAdmin, + isOwner, + }; + }, +}; + +export default provider; diff --git a/src/widgets/bookmark/BookmarkWidgetTile.tsx b/src/widgets/bookmark/BookmarkWidgetTile.tsx index 6bc739b8466..2311bbccc03 100644 --- a/src/widgets/bookmark/BookmarkWidgetTile.tsx +++ b/src/widgets/bookmark/BookmarkWidgetTile.tsx @@ -87,7 +87,7 @@ const definition = defineWidget({ return t('item.validation.length', { shortest: '1', longest: '100' }); }, href: (value) => { - if (!z.string().min(1).max(200).safeParse(value).success) { + if (!z.string().min(1).max(8192).safeParse(value).success) { return t('item.validation.length', { shortest: '1', longest: '200' }); } diff --git a/src/widgets/index.ts b/src/widgets/index.ts index b83187e2ebf..1bc9bd18fe7 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -6,6 +6,7 @@ import dnsHoleControls from './dnshole/DnsHoleControls'; import dnsHoleSummary from './dnshole/DnsHoleSummary'; import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile'; import iframe from './iframe/IFrameTile'; +import indexerManager from './indexer-manager/IndexerManagerTile'; import mediaRequestsList from './media-requests/MediaRequestListTile'; import mediaRequestsStats from './media-requests/MediaRequestStatsTile'; import mediaServer from './media-server/MediaServerTile'; @@ -20,6 +21,7 @@ import weather from './weather/WeatherTile'; export default { calendar, + 'indexer-manager': indexerManager, dashdot, usenet, weather, diff --git a/src/widgets/indexer-manager/IndexerManagerTile.tsx b/src/widgets/indexer-manager/IndexerManagerTile.tsx new file mode 100644 index 00000000000..98807fa8fd4 --- /dev/null +++ b/src/widgets/indexer-manager/IndexerManagerTile.tsx @@ -0,0 +1,98 @@ +import { Button, Card, Flex, Group, ScrollArea, Text } from '@mantine/core'; +import { IconCircleCheck, IconCircleX, IconReportSearch, IconTestPipe } from '@tabler/icons-react'; +import { useSession } from 'next-auth/react'; +import { useTranslation } from 'next-i18next'; +import { useConfigContext } from '~/config/provider'; +import { api } from '~/utils/api'; + +import { defineWidget } from '../helper'; +import { WidgetLoading } from '../loading'; +import { IWidget } from '../widgets'; + +const definition = defineWidget({ + id: 'indexer-manager', + icon: IconReportSearch, + options: {}, + gridstack: { + minWidth: 1, + minHeight: 1, + maxWidth: 3, + maxHeight: 3, + }, + component: IndexerManagerWidgetTile, +}); + +export type IIndexerManagerWidget = IWidget<(typeof definition)['id'], typeof definition>; + +interface IndexerManagerWidgetProps { + widget: IIndexerManagerWidget; +} + +function IndexerManagerWidgetTile({ widget }: IndexerManagerWidgetProps) { + const { t } = useTranslation('modules/indexer-manager'); + const { data: sessionData } = useSession(); + const { name: configName } = useConfigContext(); + const utils = api.useUtils(); + const { isLoading: testAllLoading, mutateAsync: testAllAsync } = + api.indexerManager.testAllIndexers.useMutation({ + onSuccess: async () => { + await utils.indexerManager.invalidate(); + }, + }); + const { isInitialLoading: indexersLoading, data: indexersData } = + api.indexerManager.indexers.useQuery({ + configName: configName!, + }); + const { isInitialLoading: statusesLoading, data: statusesData } = + api.indexerManager.statuses.useQuery( + { + configName: configName!, + }, + { + staleTime: 1000 * 60 * 2, + } + ); + if (indexersLoading || !indexersData || statusesLoading) { + return ; + } + + return ( + + {t('indexersStatus.title')} + + + {indexersData.map((indexer: any) => ( + + + {indexer.name} + + {!statusesData.find((status: any) => indexer.id === status.indexerId) && + indexer.enable ? ( + + ) : ( + + )} + + ))} + + + {sessionData && ( + + )} + + ); +} + +export default definition; diff --git a/src/widgets/notebook/NotebookEditor.tsx b/src/widgets/notebook/NotebookEditor.tsx index a9faf7f2df7..f5057e133a4 100644 --- a/src/widgets/notebook/NotebookEditor.tsx +++ b/src/widgets/notebook/NotebookEditor.tsx @@ -95,6 +95,13 @@ export function Editor({ widget }: { widget: INotebookWidget }) { validate(url) { return /^https?:\/\//.test(url); }, + }).extend({ + addAttributes() { + return { + ...this.parent?.(), + target: { default: null } + } + } }), StarterKit, Table.configure({ diff --git a/src/widgets/smart-home/entity-state/entity-state.widget.tsx b/src/widgets/smart-home/entity-state/entity-state.widget.tsx index c47152e983e..e448fd23518 100644 --- a/src/widgets/smart-home/entity-state/entity-state.widget.tsx +++ b/src/widgets/smart-home/entity-state/entity-state.widget.tsx @@ -16,6 +16,11 @@ const definition = defineWidget({ defaultValue: 'sun.sun', info: true, }, + appendUnit: { + type: 'switch', + defaultValue: false, + info: true, + }, automationId: { type: 'text', info: true, @@ -25,6 +30,11 @@ const definition = defineWidget({ type: 'text', defaultValue: 'Sun', }, + displayFriendlyName: { + type: 'switch', + defaultValue: false, + info: true, + }, }, gridstack: { minWidth: 1, @@ -58,6 +68,14 @@ function EntityStateTile({ widget }: SmartHomeEntityStateWidgetProps) { }, ); + const attribute = (widget.properties.appendUnit && data?.attributes.unit_of_measurement ? + " " + data?.attributes.unit_of_measurement : "" + ) + + const displayName = (widget.properties.displayFriendlyName && data?.attributes.friendly_name ? + data?.attributes.friendly_name : widget.properties.displayName + ) + const { mutateAsync: mutateTriggerAutomationAsync } = api.smartHomeEntityState.triggerAutomation.useMutation({ onSuccess: () => { void utils.smartHomeEntityState.invalidate(); @@ -101,6 +119,7 @@ function EntityStateTile({ widget }: SmartHomeEntityStateWidgetProps) { dataComponent = ( {data?.state} + {attribute} {isLoading && } ); @@ -118,7 +137,7 @@ function EntityStateTile({ widget }: SmartHomeEntityStateWidgetProps) { w="100%"> - {widget.properties.displayName} + {displayName} {dataComponent} diff --git a/tests/pages/auth/login.spec.ts b/tests/pages/auth/login.spec.ts index d4facabe3f9..da0dbfce0ee 100644 --- a/tests/pages/auth/login.spec.ts +++ b/tests/pages/auth/login.spec.ts @@ -38,6 +38,9 @@ describe('login page', () => { redirectAfterLogin: null, isDemo: false, _i18Next: 'hello', + oidcAutoLogin: null, + oidcProviderName: null, + providers: undefined }, }); @@ -75,6 +78,9 @@ describe('login page', () => { redirectAfterLogin: '/manage/users/create', isDemo: false, _i18Next: 'hello', + oidcAutoLogin: null, + oidcProviderName: null, + providers: undefined }, }); @@ -112,6 +118,9 @@ describe('login page', () => { redirectAfterLogin: null, isDemo: false, _i18Next: 'hello', + oidcAutoLogin: null, + oidcProviderName: null, + providers: undefined }, }); diff --git a/tsconfig.json b/tsconfig.json index ddd387b7f15..0266d4ed313 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "es2017", "lib": [ "dom", "dom.iterable", diff --git a/yarn.lock b/yarn.lock index 7d145496753..26f58fa16d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,34 +22,6 @@ __metadata: languageName: node linkType: hard -"@auth/core@npm:0.18.1": - version: 0.18.1 - resolution: "@auth/core@npm:0.18.1" - dependencies: - "@panva/hkdf": ^1.1.1 - cookie: 0.5.0 - jose: ^5.1.0 - oauth4webapi: ^2.3.0 - preact: 10.11.3 - preact-render-to-string: 5.2.3 - peerDependencies: - nodemailer: ^6.8.0 - peerDependenciesMeta: - nodemailer: - optional: true - checksum: 46ae80e621e03d9206cc9a5e37941df92207e58298f423ec71ae2b8d3492d86f14d5e024ba30c5a905675c451688d212d389b580748f3a176ec0ddcd3872291a - languageName: node - linkType: hard - -"@auth/drizzle-adapter@npm:^0.3.2": - version: 0.3.6 - resolution: "@auth/drizzle-adapter@npm:0.3.6" - dependencies: - "@auth/core": 0.18.1 - checksum: c80abc825ab15645f39ad4fd630ca81caf18880aca32f8df030a072dfb7f5222d1fe4396713041bf24e7252c8478a09be81ac4f921652497319acf30e138f4ec - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.22.13": version: 7.22.13 resolution: "@babel/code-frame@npm:7.22.13" @@ -1033,6 +1005,95 @@ __metadata: languageName: node linkType: hard +"@ldapjs/asn1@npm:2.0.0, @ldapjs/asn1@npm:^2.0.0": + version: 2.0.0 + resolution: "@ldapjs/asn1@npm:2.0.0" + checksum: b9957b47b14ef0a24fa5275849b624f8a1a7708c2f37b0b7ff278062527a7c93a885c9a73462c3bba4a9e182bd0766422f08597361cdbf3ecafd7dfb478ab490 + languageName: node + linkType: hard + +"@ldapjs/asn1@npm:^1.2.0": + version: 1.2.0 + resolution: "@ldapjs/asn1@npm:1.2.0" + checksum: 720b65fd825b414f672264c19edf2b67f643bd655ac9dae761394f40e332c68fbe7f442046daf88a00a656ca2cbbfe91c0435fc59c9b7c301770ea0d2606b89a + languageName: node + linkType: hard + +"@ldapjs/attribute@npm:1.0.0, @ldapjs/attribute@npm:^1.0.0": + version: 1.0.0 + resolution: "@ldapjs/attribute@npm:1.0.0" + dependencies: + "@ldapjs/asn1": 2.0.0 + "@ldapjs/protocol": ^1.2.1 + process-warning: ^2.1.0 + checksum: 887665a3067deebbfea7760befc535f94205f87cece0f164f9ddc2f3f5b0daa136a0ede4520fa37aa9d30af025cb23023a155473bbc61916aa39da2ad697c7f0 + languageName: node + linkType: hard + +"@ldapjs/change@npm:^1.0.0": + version: 1.0.0 + resolution: "@ldapjs/change@npm:1.0.0" + dependencies: + "@ldapjs/asn1": 2.0.0 + "@ldapjs/attribute": 1.0.0 + checksum: 5f28d8e904fe47cbaff225d9696d35ee78f1f648e2aedab9aebe67c0b19df4a9b0224bf2ac9a8ab2d1dab00e69eaff9f17be5532af2f32862e27a973228d83eb + languageName: node + linkType: hard + +"@ldapjs/controls@npm:^2.1.0": + version: 2.1.0 + resolution: "@ldapjs/controls@npm:2.1.0" + dependencies: + "@ldapjs/asn1": ^1.2.0 + "@ldapjs/protocol": ^1.2.1 + checksum: b61a69ddf0634ea6bbc1a32691fa19ee92aa2efe17aeae77ea261b5b16cf6102c36ed71ef0ce038ec74fe7751917c0946862fdabe328086b7561b6e6453ef794 + languageName: node + linkType: hard + +"@ldapjs/dn@npm:^1.1.0": + version: 1.1.0 + resolution: "@ldapjs/dn@npm:1.1.0" + dependencies: + "@ldapjs/asn1": 2.0.0 + process-warning: ^2.1.0 + checksum: 716e408c9f8ea1d1f14c512a1ecbc3271d7873da1aee788bfa6548a47290fecefd9ea2039f1f9f9238cba8072ae798c4e4b4da5e457ee24b68e94572665f711f + languageName: node + linkType: hard + +"@ldapjs/filter@npm:^2.1.1": + version: 2.1.1 + resolution: "@ldapjs/filter@npm:2.1.1" + dependencies: + "@ldapjs/asn1": 2.0.0 + "@ldapjs/protocol": ^1.2.1 + process-warning: ^2.1.0 + checksum: e87c698fe7921969a751479b435a58f8202ebbe48420a3705dd47180b33ae39fcbe1451640c58fb94f80b4a96efa91d99ec91e6dc6d7be96b7bc3cc469506ba8 + languageName: node + linkType: hard + +"@ldapjs/messages@npm:^1.3.0": + version: 1.3.0 + resolution: "@ldapjs/messages@npm:1.3.0" + dependencies: + "@ldapjs/asn1": ^2.0.0 + "@ldapjs/attribute": ^1.0.0 + "@ldapjs/change": ^1.0.0 + "@ldapjs/controls": ^2.1.0 + "@ldapjs/dn": ^1.1.0 + "@ldapjs/filter": ^2.1.1 + "@ldapjs/protocol": ^1.2.1 + process-warning: ^2.2.0 + checksum: e7f1994db976456546769d72b2efba18c93e9201c81050b52479575bf72bac42312c6b817e886ac315caf592b00d2f0d3407fcc4eea58ff65c8bd18211e5b458 + languageName: node + linkType: hard + +"@ldapjs/protocol@npm:^1.2.1": + version: 1.2.1 + resolution: "@ldapjs/protocol@npm:1.2.1" + checksum: 3e26f3fc642897ae1448a5a172839ab368fe72e05b9eaf36e16fe6dd4c3c93ce298ab4e3907b3eda9c3911e018d617777b79071dfbb3b813add56b046aea48dc + languageName: node + linkType: hard + "@mantine/core@npm:^6.0.0": version: 6.0.21 resolution: "@mantine/core@npm:6.0.21" @@ -1504,7 +1565,7 @@ __metadata: languageName: node linkType: hard -"@panva/hkdf@npm:^1.0.2, @panva/hkdf@npm:^1.1.1": +"@panva/hkdf@npm:^1.0.2": version: 1.1.1 resolution: "@panva/hkdf@npm:1.1.1" checksum: f0dd12903751d8792420353f809ed3c7de860cf506399759fff5f59f7acfef8a77e2b64012898cee7e5b047708fa0bd91dff5ef55a502bf8ea11aad9842160da @@ -3286,6 +3347,15 @@ __metadata: languageName: node linkType: hard +"@types/ldapjs@npm:^3.0.2": + version: 3.0.2 + resolution: "@types/ldapjs@npm:3.0.2" + dependencies: + "@types/node": "*" + checksum: 0839acb3c46aa231577266c46700b44cfeb5cc77cfb854be6dac25bf1346cd0b5c83e3671fd6a78769c7702a1eb610d5f08b68e2583bb5c13d214eb0558c3d36 + languageName: node + linkType: hard + "@types/mime@npm:*": version: 3.0.4 resolution: "@types/mime@npm:3.0.4" @@ -3919,6 +3989,13 @@ __metadata: languageName: node linkType: hard +"abstract-logging@npm:^2.0.1": + version: 2.0.1 + resolution: "abstract-logging@npm:2.0.1" + checksum: 6967d15e5abbafd17f56eaf30ba8278c99333586fa4f7935fd80e93cfdc006c37fcc819c5d63ee373a12e6cb2d0417f7c3c6b9e42b957a25af9937d26749415e + languageName: node + linkType: hard + "accepts@npm:^1.3.7": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -4207,6 +4284,13 @@ __metadata: languageName: node linkType: hard +"assert-plus@npm:^1.0.0": + version: 1.0.0 + resolution: "assert-plus@npm:1.0.0" + checksum: 19b4340cb8f0e6a981c07225eacac0e9d52c2644c080198765d63398f0075f83bbc0c8e95474d54224e297555ad0d631c1dcd058adb1ddc2437b41a6b424ac64 + languageName: node + linkType: hard + "assertion-error@npm:^1.1.0": version: 1.1.0 resolution: "assertion-error@npm:1.1.0" @@ -4309,6 +4393,15 @@ __metadata: languageName: node linkType: hard +"backoff@npm:^2.5.0": + version: 2.5.0 + resolution: "backoff@npm:2.5.0" + dependencies: + precond: 0.2 + checksum: ccdcf2a26acd9379d0d4f09e3fb3b7ee34dee94f07ab74d1e38b38f89a3675d9f3cbebb142d9c61c655f4c9eb63f1d6ec28cebeb3dc9215efd8fe7cef92725b9 + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -4904,13 +4997,6 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.5.0, cookie@npm:^0.5.0": - version: 0.5.0 - resolution: "cookie@npm:0.5.0" - checksum: 1f4bd2ca5765f8c9689a7e8954183f5332139eb72b6ff783d8947032ec1fdf43109852c178e21a953a30c0dd42257828185be01b49d1eb1a67fd054ca588a180 - languageName: node - linkType: hard - "cookie@npm:^0.4.0": version: 0.4.2 resolution: "cookie@npm:0.4.2" @@ -4918,6 +5004,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^0.5.0": + version: 0.5.0 + resolution: "cookie@npm:0.5.0" + checksum: 1f4bd2ca5765f8c9689a7e8954183f5332139eb72b6ff783d8947032ec1fdf43109852c178e21a953a30c0dd42257828185be01b49d1eb1a67fd054ca588a180 + languageName: node + linkType: hard + "cookie@npm:~0.6.0": version: 0.6.0 resolution: "cookie@npm:0.6.0" @@ -4978,6 +5071,13 @@ __metadata: languageName: node linkType: hard +"core-util-is@npm:1.0.2": + version: 1.0.2 + resolution: "core-util-is@npm:1.0.2" + checksum: 7a4c925b497a2c91421e25bf76d6d8190f0b2359a9200dbeed136e63b2931d6294d3b1893eda378883ed363cd950f44a12a401384c609839ea616befb7927dab + languageName: node + linkType: hard + "core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" @@ -6481,6 +6581,13 @@ __metadata: languageName: node linkType: hard +"extsprintf@npm:^1.2.0": + version: 1.4.1 + resolution: "extsprintf@npm:1.4.1" + checksum: a2f29b241914a8d2bad64363de684821b6b1609d06ae68d5b539e4de6b28659715b5bea94a7265201603713b7027d35399d10b0548f09071c5513e65e8323d33 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -7276,7 +7383,6 @@ __metadata: version: 0.0.0-use.local resolution: "homarr@workspace:." dependencies: - "@auth/drizzle-adapter": ^0.3.2 "@ctrl/deluge": ^4.1.0 "@ctrl/qbittorrent": ^6.0.0 "@ctrl/shared-torrent": ^4.1.1 @@ -7327,6 +7433,7 @@ __metadata: "@types/better-sqlite3": ^7.6.5 "@types/cookies": ^0.7.7 "@types/dockerode": ^3.3.9 + "@types/ldapjs": ^3.0.2 "@types/node": 18.17.8 "@types/prismjs": ^1.26.0 "@types/react": ^18.2.11 @@ -7370,9 +7477,8 @@ __metadata: i18next: ^22.5.1 immer: ^10.0.2 js-file-download: ^0.4.12 + ldapjs: ^3.0.5 mantine-react-table: ^1.3.4 - moment: ^2.29.4 - moment-timezone: ^0.5.43 next: 13.4.12 next-auth: ^4.23.0 next-i18next: ^14.0.0 @@ -8188,13 +8294,6 @@ __metadata: languageName: node linkType: hard -"jose@npm:^5.1.0": - version: 5.1.1 - resolution: "jose@npm:5.1.1" - checksum: 3a18d85dd1ed0e7746c67cba65a95ee972f20b363ceb99a9d75b870beb34942089cfca6249c4a50a79bc854c5a052f1be39e814c42b0f00f9358e902ce706e8d - languageName: node - linkType: hard - "js-file-download@npm:^0.4.12": version: 0.4.12 resolution: "js-file-download@npm:0.4.12" @@ -8398,6 +8497,28 @@ __metadata: languageName: node linkType: hard +"ldapjs@npm:^3.0.5": + version: 3.0.7 + resolution: "ldapjs@npm:3.0.7" + dependencies: + "@ldapjs/asn1": ^2.0.0 + "@ldapjs/attribute": ^1.0.0 + "@ldapjs/change": ^1.0.0 + "@ldapjs/controls": ^2.1.0 + "@ldapjs/dn": ^1.1.0 + "@ldapjs/filter": ^2.1.1 + "@ldapjs/messages": ^1.3.0 + "@ldapjs/protocol": ^1.2.1 + abstract-logging: ^2.0.1 + assert-plus: ^1.0.0 + backoff: ^2.5.0 + once: ^1.4.0 + vasync: ^2.2.1 + verror: ^1.10.1 + checksum: 4c0c4aeb5a0e22d0b1cba3779663472d8ebe6bc0fed5e56d6e29ac15b7f9e567e673c8764d0e51ca52eab48eef2024561a3553d6c804b11a260a893c18bd8df7 + languageName: node + linkType: hard + "levn@npm:^0.4.1": version: 0.4.1 resolution: "levn@npm:0.4.1" @@ -8960,22 +9081,6 @@ __metadata: languageName: node linkType: hard -"moment-timezone@npm:^0.5.43": - version: 0.5.43 - resolution: "moment-timezone@npm:0.5.43" - dependencies: - moment: ^2.29.4 - checksum: 8075c897ed8a044f992ef26fe8cdbcad80caf974251db424cae157473cca03be2830de8c74d99341b76edae59f148c9d9d19c1c1d9363259085688ec1cf508d0 - languageName: node - linkType: hard - -"moment@npm:^2.29.4": - version: 2.29.4 - resolution: "moment@npm:2.29.4" - checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e - languageName: node - linkType: hard - "mpd-parser@npm:^1.0.1, mpd-parser@npm:^1.2.2": version: 1.2.2 resolution: "mpd-parser@npm:1.2.2" @@ -9341,13 +9446,6 @@ __metadata: languageName: node linkType: hard -"oauth4webapi@npm:^2.3.0": - version: 2.4.0 - resolution: "oauth4webapi@npm:2.4.0" - checksum: 9e6d5be3966013aa9dd61781032a6bd07a63166a9819f2fc0d622d33b23221ea39ae25334a4bde9eba4623e576972d367b196e3b5d3facff75002125c510b672 - languageName: node - linkType: hard - "oauth@npm:^0.9.15": version: 0.9.15 resolution: "oauth@npm:0.9.15" @@ -9801,17 +9899,6 @@ __metadata: languageName: node linkType: hard -"preact-render-to-string@npm:5.2.3": - version: 5.2.3 - resolution: "preact-render-to-string@npm:5.2.3" - dependencies: - pretty-format: ^3.8.0 - peerDependencies: - preact: ">=10" - checksum: 6e46288d8956adde35b9fe3a21aecd9dea29751b40f0f155dea62f3896f27cb8614d457b32f48d33909d2da81135afcca6c55077520feacd7d15164d1371fb44 - languageName: node - linkType: hard - "preact-render-to-string@npm:^5.1.19": version: 5.2.6 resolution: "preact-render-to-string@npm:5.2.6" @@ -9823,13 +9910,6 @@ __metadata: languageName: node linkType: hard -"preact@npm:10.11.3": - version: 10.11.3 - resolution: "preact@npm:10.11.3" - checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367 - languageName: node - linkType: hard - "preact@npm:^10.6.3": version: 10.19.2 resolution: "preact@npm:10.19.2" @@ -9859,6 +9939,13 @@ __metadata: languageName: node linkType: hard +"precond@npm:0.2": + version: 0.2.3 + resolution: "precond@npm:0.2.3" + checksum: c613e7d68af3e0b43a294a994bf067cc2bc44b03fd17bc4fb133e30617a4f5b49414b08e9b392d52d7c6822d8a71f66a7fe93a8a1e7d02240177202cff3f63ef + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -9941,6 +10028,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^2.1.0, process-warning@npm:^2.2.0": + version: 2.3.2 + resolution: "process-warning@npm:2.3.2" + checksum: cbeddc85d3963eccd6578b1eea5ba981383d1ec688d6e4ba5bf0ca6662d094c024b44dfcb1c530662c7694b68fe09fd95fa0269a1309090d793008f4553e7784 + languageName: node + linkType: hard + "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -12520,6 +12614,37 @@ __metadata: languageName: node linkType: hard +"vasync@npm:^2.2.1": + version: 2.2.1 + resolution: "vasync@npm:2.2.1" + dependencies: + verror: 1.10.0 + checksum: dca14090436f1b30d4887737af47bc8333795a6d45e520e583ca2c4476d841bf68606cbc79071cfd980e3e42e630736d66a598b9100a505663442ae2e7c2f92f + languageName: node + linkType: hard + +"verror@npm:1.10.0": + version: 1.10.0 + resolution: "verror@npm:1.10.0" + dependencies: + assert-plus: ^1.0.0 + core-util-is: 1.0.2 + extsprintf: ^1.2.0 + checksum: c431df0bedf2088b227a4e051e0ff4ca54df2c114096b0c01e1cbaadb021c30a04d7dd5b41ab277bcd51246ca135bf931d4c4c796ecae7a4fef6d744ecef36ea + languageName: node + linkType: hard + +"verror@npm:^1.10.1": + version: 1.10.1 + resolution: "verror@npm:1.10.1" + dependencies: + assert-plus: ^1.0.0 + core-util-is: 1.0.2 + extsprintf: ^1.2.0 + checksum: 690a8d6ad5a4001672290e9719e3107c86269bc45fe19f844758eecf502e59f8aa9631b19b839f6d3dea562334884d22d1eb95ae7c863032075a9212c889e116 + languageName: node + linkType: hard + "video.js@npm:^7 || ^8, video.js@npm:^8.0.3": version: 8.6.1 resolution: "video.js@npm:8.6.1"