From 4933b4f60d053b1afb23b6d727abb942257779cf Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:18:16 +0100 Subject: [PATCH 01/10] docs: add argos ci to readme (#1883) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f106b66adde..8c2fcbb3ed2 100644 --- a/README.md +++ b/README.md @@ -123,3 +123,5 @@ 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) From cefa0d8fdec2451773bce03ed3a139ec317b578e Mon Sep 17 00:00:00 2001 From: Tagaishi Date: Fri, 9 Feb 2024 22:28:23 +0100 Subject: [PATCH 02/10] fix: notebook link target bug fix (#1889) --- src/widgets/notebook/NotebookEditor.tsx | 7 +++++++ 1 file changed, 7 insertions(+) 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({ From 02249d20c2578edaea3eaea1507ec8e854433886 Mon Sep 17 00:00:00 2001 From: Tagaishi Date: Fri, 9 Feb 2024 22:30:03 +0100 Subject: [PATCH 03/10] fix: add check for already existing name upon creating board (#1887) --- .../Manage/Board/create-board.modal.tsx | 8 ++++-- src/server/api/routers/config.ts | 27 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) 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<{}>) => { + )} + + ); +} + +export default definition; From f0a67d9a29b88ecaed27ab6b3ee840c03c98af27 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 9 Feb 2024 22:36:34 +0100 Subject: [PATCH 06/10] feat: allow up to 8128 characters for links in bookmark widget (#1851) #1850 --- src/widgets/bookmark/BookmarkWidgetTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' }); } From b1ae5f700e4b296b157e4230a16df4e387860865 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 9 Feb 2024 22:37:26 +0100 Subject: [PATCH 07/10] chore(translations): new Crowdin updates (#1878) --- public/locales/it/modules/rss.json | 4 +-- public/locales/lv/common.json | 4 +-- .../lv/layout/element-selector/selector.json | 2 +- public/locales/lv/manage/boards.json | 10 +++--- public/locales/lv/manage/users.json | 8 ++--- public/locales/lv/manage/users/edit.json | 32 +++++++++---------- public/locales/lv/modules/date.json | 14 ++++---- public/locales/lv/modules/rss.json | 6 ++-- .../lv/modules/smart-home/entity-state.json | 18 +++++------ .../smart-home/trigger-automation.json | 12 +++---- .../locales/lv/modules/torrents-status.json | 18 +++++------ .../customization/page-appearance.json | 22 ++++++------- public/locales/lv/tools/docker.json | 2 +- public/locales/sk/modules/rss.json | 4 +-- public/locales/tr/common.json | 4 +-- 15 files changed, 80 insertions(+), 80 deletions(-) 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}}", From 9a8ea9e1fe1943519a873c29e5ab939ba318130d Mon Sep 17 00:00:00 2001 From: Rikpat <33869814+Rikpat@users.noreply.github.com> Date: Fri, 9 Feb 2024 22:57:00 +0100 Subject: [PATCH 08/10] feat: add ldap and oidc support (#1497) Co-authored-by: Thomas Camlong <49837342+ajnart@users.noreply.github.com> Co-authored-by: Tagaishi --- next.config.js | 5 + package.json | 5 +- .../Manage/User/Create/review-input-step.tsx | 2 +- .../Onboarding/step-create-account.tsx | 9 +- src/env.js | 60 ++++ src/middleware.ts | 16 +- src/pages/auth/invite/[inviteId].tsx | 3 +- src/pages/auth/login.tsx | 178 +++++++---- src/server/auth.ts | 108 +------ src/server/db/schema.ts | 8 +- src/utils/auth/adapter.ts | 166 ++++++++++ src/utils/auth/credentials.ts | 56 ++++ src/utils/auth/index.ts | 46 +++ src/utils/auth/ldap.ts | 161 ++++++++++ src/utils/auth/oidc.ts | 51 +++ tests/pages/auth/login.spec.ts | 9 + tsconfig.json | 2 +- yarn.lock | 299 +++++++++++++----- 18 files changed, 929 insertions(+), 255 deletions(-) create mode 100644 src/utils/auth/adapter.ts create mode 100644 src/utils/auth/credentials.ts create mode 100644 src/utils/auth/index.ts create mode 100644 src/utils/auth/ldap.ts create mode 100644 src/utils/auth/oidc.ts 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..ef83e4af84d 100644 --- a/package.json +++ b/package.json @@ -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/src/components/Manage/User/Create/review-input-step.tsx b/src/components/Manage/User/Create/review-input-step.tsx index eb56d7efe94..7d819b19db4 100644 --- a/src/components/Manage/User/Create/review-input-step.tsx +++ b/src/components/Manage/User/Create/review-input-step.tsx @@ -111,7 +111,7 @@ export const ReviewInputStep = ({ values, prevStep, nextStep }: ReviewInputStepP password: values.security.password, email: values.account.eMail === '' ? undefined : values.account.eMail, }); - umami.track('Create user', { username: values.account.username}); + umami.track('Create user', { username: values.account.username }); }} loading={isLoading} rightIcon={} diff --git a/src/components/Onboarding/step-create-account.tsx b/src/components/Onboarding/step-create-account.tsx index 01dc4a5f865..f5ada5cb083 100644 --- a/src/components/Onboarding/step-create-account.tsx +++ b/src/components/Onboarding/step-create-account.tsx @@ -57,9 +57,12 @@ export const StepCreateAccount = ({ Create your administrator account - Your administrator account must be secure, that's why we have so many rules surrounding it. -
Try not to make it adminadmin this time... -
Note: these password requirements are not forced, they are just recommendations. + Your administrator account must be secure, that's why we have so many rules + surrounding it. +
+ Try not to make it adminadmin this time... +
+ Note: these password requirements are not forced, they are just recommendations.
diff --git a/src/env.js b/src/env.js index 1787aa5cefe..1b7c349cedc 100644 --- a/src/env.js +++ b/src/env.js @@ -1,6 +1,14 @@ const { z } = require('zod'); const { createEnv } = require('@t3-oss/env-nextjs'); +const trueStrings = ["1", "t", "T", "TRUE", "true", "True"]; +const falseStrings = ["0", "f", "F", "FALSE", "false", "False"]; + +const zodParsedBoolean = () => z + .enum([...trueStrings, ...falseStrings]) + .default("false") + .transform((value) => trueStrings.includes(value)) + const portSchema = z .string() .regex(/\d*/) @@ -8,6 +16,8 @@ const portSchema = z .optional(); const envSchema = z.enum(['development', 'test', 'production']); +const authProviders = process.env.AUTH_PROVIDER?.replaceAll(' ', '').split(',') || ['credentials']; + const env = createEnv({ /** * Specify your server-side environment variables schema here. This way you can ensure the app @@ -28,6 +38,37 @@ const env = createEnv({ DOCKER_PORT: portSchema, DEMO_MODE: z.string().optional(), HOSTNAME: z.string().optional(), + + // Authentication + AUTH_PROVIDER: z.string().default('credentials').transform(providers => providers.replaceAll(' ', '').split(',')), + // LDAP + ...(authProviders.includes('ldap') + ? { + AUTH_LDAP_URI: z.string().url(), + AUTH_LDAP_BIND_DN: z.string(), + AUTH_LDAP_BIND_PASSWORD: z.string(), + AUTH_LDAP_BASE: z.string(), + AUTH_LDAP_USERNAME_ATTRIBUTE: z.string().default('uid'), + AUTH_LDAP_GROUP_CLASS: z.string().default('groupOfUniqueNames'), + AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE: z.string().default('member'), + AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE: z.string().default('dn'), + AUTH_LDAP_ADMIN_GROUP: z.string().default('admin'), + AUTH_LDAP_OWNER_GROUP: z.string().default('admin'), + } + : {}), + // OIDC + ...(authProviders.includes('oidc') + ? { + AUTH_OIDC_CLIENT_ID: z.string(), + AUTH_OIDC_CLIENT_SECRET: z.string(), + AUTH_OIDC_URI: z.string().url(), + // Custom Display name, defaults to OIDC + AUTH_OIDC_CLIENT_NAME: z.string().default('OIDC'), + AUTH_OIDC_ADMIN_GROUP: z.string().default('admin'), + AUTH_OIDC_OWNER_GROUP: z.string().default('admin'), + AUTH_OIDC_AUTO_LOGIN: zodParsedBoolean() + } + : {}), }, /** @@ -64,6 +105,25 @@ const env = createEnv({ NEXT_PUBLIC_PORT: process.env.PORT, NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV, HOSTNAME: process.env.HOSTNAME, + AUTH_PROVIDER: process.env.AUTH_PROVIDER, + AUTH_LDAP_URI: process.env.AUTH_LDAP_URI, + AUTH_LDAP_BIND_DN: process.env.AUTH_LDAP_BIND_DN, + AUTH_LDAP_BIND_PASSWORD: process.env.AUTH_LDAP_BIND_PASSWORD, + AUTH_LDAP_BASE: process.env.AUTH_LDAP_BASE, + AUTH_LDAP_USERNAME_ATTRIBUTE: process.env.AUTH_LDAP_USERNAME_ATTRIBUTE, + AUTH_LDAP_GROUP_CLASS: process.env.AUTH_LDAP_GROUP_CLASS, + AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE: process.env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE, + AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE: process.env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE, + AUTH_LDAP_ADMIN_GROUP: process.env.AUTH_LDAP_ADMIN_GROUP, + AUTH_LDAP_OWNER_GROUP: process.env.AUTH_LDAP_OWNER_GROUP, + AUTH_OIDC_CLIENT_ID: process.env.AUTH_OIDC_CLIENT_ID, + AUTH_OIDC_CLIENT_SECRET: process.env.AUTH_OIDC_CLIENT_SECRET, + AUTH_OIDC_URI: process.env.AUTH_OIDC_URI, + AUTH_OIDC_CLIENT_NAME: process.env.AUTH_OIDC_CLIENT_NAME, + AUTH_OIDC_GROUP_CLAIM: process.env.AUTH_OIDC_GROUP_CLAIM, + AUTH_OIDC_ADMIN_GROUP: process.env.AUTH_OIDC_ADMIN_GROUP, + AUTH_OIDC_OWNER_GROUP: process.env.AUTH_OIDC_OWNER_GROUP, + AUTH_OIDC_AUTO_LOGIN: process.env.AUTH_OIDC_AUTO_LOGIN, DEMO_MODE: process.env.DEMO_MODE, }, skipValidation: !!process.env.SKIP_ENV_VALIDATION, diff --git a/src/middleware.ts b/src/middleware.ts index ee139deca3e..61bb29cd087 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -10,6 +10,7 @@ const skippedUrls = [ '/favicon.ico', '/404', '/pages/_app', + '/auth/login', '/imgs/', ]; @@ -29,12 +30,15 @@ export async function middleware(req: NextRequest) { } // Do not redirect if there are users in the database - if (cachedUserCount > 0) { - return NextResponse.next(); - } - - // Do not redirect if there are users in the database - if (!(await shouldRedirectToOnboard())) { + if (cachedUserCount > 0 || !(await shouldRedirectToOnboard())) { + // redirect to login if not logged in + // not working, should work in next-auth 5 + // @see https://github.com/nextauthjs/next-auth/pull/7443 + + // const session = await getServerSession(); + // if (!session?.user) { + // return NextResponse.redirect(getUrl(req) + '/auth/login') + // } return NextResponse.next(); } diff --git a/src/pages/auth/invite/[inviteId].tsx b/src/pages/auth/invite/[inviteId].tsx index f41bfeb6a28..32d0e3bafec 100644 --- a/src/pages/auth/invite/[inviteId].tsx +++ b/src/pages/auth/invite/[inviteId].tsx @@ -125,8 +125,7 @@ export default function AuthInvitePage() { withAsterisk {...form.getInputProps('password')} /> - + diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index b1dabe22c49..eb7f0af2efc 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -1,13 +1,24 @@ -import { Alert, Button, Card, Flex, PasswordInput, Stack, Text, TextInput, Title } from '@mantine/core'; +import { + Alert, + Button, + Card, + Divider, + Flex, + PasswordInput, + Stack, + Text, + TextInput, + Title, +} from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconAlertTriangle } from '@tabler/icons-react'; -import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { signIn } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; import Head from 'next/head'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { z } from 'zod'; import { ThemeSchemeToggle } from '~/components/ThemeSchemeToggle/ThemeSchemeToggle'; import { FloatingBackground } from '~/components/layout/Background/FloatingBackground'; @@ -17,8 +28,13 @@ import { getServerSideTranslations } from '~/tools/server/getServerSideTranslati import { useI18nZodResolver } from '~/utils/i18n-zod-resolver'; import { signInSchema } from '~/validations/user'; +const signInSchemaWithProvider = signInSchema.extend({ provider: z.string() }); + export default function LoginPage({ redirectAfterLogin, + providers, + oidcProviderName, + oidcAutoLogin, isDemo, }: InferGetServerSidePropsType) { const { t } = useTranslation('authentication/login'); @@ -27,16 +43,18 @@ export default function LoginPage({ const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); - const form = useForm>({ + const hasCredentialsInput = providers.includes('credentials') || providers.includes('ldap'); + + const form = useForm>({ validateInputOnChange: true, validateInputOnBlur: true, - validate: i18nZodResolver(signInSchema), + validate: i18nZodResolver(signInSchemaWithProvider), }); - const handleSubmit = (values: z.infer) => { + const handleSubmit = (values: z.infer) => { setIsLoading(true); setIsError(false); - signIn('credentials', { + signIn(values.provider, { redirect: false, name: values.name, password: values.password, @@ -51,6 +69,10 @@ export default function LoginPage({ }); }; + useEffect(() => { + if (oidcAutoLogin) signIn('oidc'); + }, [oidcAutoLogin]); + const metaTitle = `${t('metaTitle')} • Homarr`; return ( @@ -58,7 +80,6 @@ export default function LoginPage({ {metaTitle} - @@ -83,51 +104,94 @@ export default function LoginPage({ demodemo )} - - - {t('title')} - - - - {t('text')} - - - {isError && ( - } color="red"> - {t('alert')} - - )} - - - - - - - - + )} + + {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/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/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/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" From 508f687491cca006713f92ab48c096ebfa0ef7e9 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sat, 10 Feb 2024 15:31:59 +0100 Subject: [PATCH 09/10] Update README.md Fixed incorrect URLs in the integrations part of the README #1891 --- README.md | 57 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 8c2fcbb3ed2..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,5 +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) From 46a57c1cf2bdcfeb99171cac253585496447eb47 Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:36:09 +0100 Subject: [PATCH 10/10] config: tag release (#1906) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef83e4af84d..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": {