Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46a57c1cf2 | ||
|
|
78527cb375 | ||
|
|
508f687491 | ||
|
|
9a8ea9e1fe | ||
|
|
b1ae5f700e | ||
|
|
f0a67d9a29 | ||
|
|
5d113ea280 | ||
|
|
d45ae5fab9 | ||
|
|
02249d20c2 | ||
|
|
cefa0d8fde | ||
|
|
4933b4f60d |
55
README.md
55
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
|
- 🦞 Comprehensive built-in icon picker with over 7000 icons
|
||||||
- 🐳 Easy deployment with Docker, unRAID, and Synology
|
- 🐳 Easy deployment with Docker, unRAID, and Synology
|
||||||
- 🚀 Compatible with any major consumer hardware (x86, Raspberry Pi, old laptops, ...)
|
- 🚀 Compatible with any major consumer hardware (x86, Raspberry Pi, old laptops, ...)
|
||||||
- 💵 Free and Open-Source - your data stays on your device. No telemetry data.
|
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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.
|
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 for your comfort.
|
Each widget and integration has a comprehensive documentation
|
||||||
Homarr will integrate with the following applications of yours:
|
Homarr will integrate with the following applications:
|
||||||
|
|
||||||
- 📥 Torrent clients
|
📥 Torrent clients
|
||||||
- [Deluge](https://homarr.dev/docs/integrations/#deluge)
|
- [Deluge](https://homarr.dev/docs/management/integrations/torrent-deluge)
|
||||||
- [Transmission](https://homarr.dev/docs/integrations/#transmission)
|
- [Transmission](https://homarr.dev/docs/management/integrations/torrent-transmission)
|
||||||
- [qBittorent](https://homarr.dev/docs/integrations/#qbittorrent-integration)
|
- [qBittorent](https://homarr.dev/docs/management/integrations/torrent-qbittorrent)
|
||||||
- 📥 Usenet clients
|
|
||||||
- [SABnzbd](https://homarr.dev/docs/integrations/#sabnzbd)
|
📥 Usenet clients
|
||||||
- [NZBGet](https://homarr.dev/docs/integrations/#nzbget)
|
- [SABnzbd](https://homarr.dev/docs/management/integrations/usenet-sabnzbd)
|
||||||
- 📚 Media collection managers
|
- [NZBGet](https://homarr.dev/docs/management/integrations/usenet-nzbget)
|
||||||
- [Sonarr](https://homarr.dev/docs/integrations/#sonarr)
|
|
||||||
- [Radarr](https://homarr.dev/docs/integrations/#radarr)
|
📺 Media servers
|
||||||
- [Lidarr](https://homarr.dev/docs/integrations/#lidarr)
|
- [Plex](https://homarr.dev/docs/management/integrations/media-server-plex)
|
||||||
- [Readarr](https://homarr.dev/docs/integrations/#readarr)
|
- [Jellyfin](https://homarr.dev/docs/management/integrations/media-server-jellyfin)
|
||||||
- 🎞️ Media request managers
|
|
||||||
- [Overseerr](https://homarr.dev/docs/integrations/#overseerr--jellyseerr)
|
📚 Media collection managers
|
||||||
- [Jellyseerr](https://homarr.dev/docs/integrations/#overseerr--jellyseerr)
|
- [Sonarr](https://homarr.dev/docs/management/integrations/servarr-sonarr)
|
||||||
- 🔌 [Dash.](https://homarr.dev/docs/integrations/#dash)
|
- [Radarr](https://homarr.dev/docs/management/integrations/servarr-radarr)
|
||||||
- 🐳 [Docker](https://homarr.dev/docs/integrations/#docker)
|
- [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.
|
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 ❤️
|
All contributions, regardless of their size or scope, are welcome and highly appreciated! Thank you ❤️
|
||||||
|
|
||||||

|

|
||||||
|
[](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss)
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = withBundleAnalyzer({
|
module.exports = withBundleAnalyzer({
|
||||||
|
webpack: (config) => {
|
||||||
|
// for dynamic loading of auth providers
|
||||||
|
config.experiments = { ...config.experiments, topLevelAwait: true };
|
||||||
|
return config;
|
||||||
|
},
|
||||||
images: {
|
images: {
|
||||||
domains: ['cdn.jsdelivr.net'],
|
domains: ['cdn.jsdelivr.net'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "0.14.6",
|
"version": "0.15.0",
|
||||||
"description": "Homarr - A homepage for your server.",
|
"description": "Homarr - A homepage for your server.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
"db:migrate": "dotenv ts-node drizzle/migrate/migrate.ts ./drizzle"
|
"db:migrate": "dotenv ts-node drizzle/migrate/migrate.ts ./drizzle"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/drizzle-adapter": "^0.3.2",
|
|
||||||
"@ctrl/deluge": "^4.1.0",
|
"@ctrl/deluge": "^4.1.0",
|
||||||
"@ctrl/qbittorrent": "^6.0.0",
|
"@ctrl/qbittorrent": "^6.0.0",
|
||||||
"@ctrl/shared-torrent": "^4.1.1",
|
"@ctrl/shared-torrent": "^4.1.1",
|
||||||
@@ -92,9 +91,8 @@
|
|||||||
"i18next": "^22.5.1",
|
"i18next": "^22.5.1",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
|
"ldapjs": "^3.0.5",
|
||||||
"mantine-react-table": "^1.3.4",
|
"mantine-react-table": "^1.3.4",
|
||||||
"moment": "^2.29.4",
|
|
||||||
"moment-timezone": "^0.5.43",
|
|
||||||
"next": "13.4.12",
|
"next": "13.4.12",
|
||||||
"next-auth": "^4.23.0",
|
"next-auth": "^4.23.0",
|
||||||
"next-i18next": "^14.0.0",
|
"next-i18next": "^14.0.0",
|
||||||
@@ -123,6 +121,7 @@
|
|||||||
"@types/better-sqlite3": "^7.6.5",
|
"@types/better-sqlite3": "^7.6.5",
|
||||||
"@types/cookies": "^0.7.7",
|
"@types/cookies": "^0.7.7",
|
||||||
"@types/dockerode": "^3.3.9",
|
"@types/dockerode": "^3.3.9",
|
||||||
|
"@types/ldapjs": "^3.0.2",
|
||||||
"@types/node": "18.17.8",
|
"@types/node": "18.17.8",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react": "^18.2.11",
|
"@types/react": "^18.2.11",
|
||||||
|
|||||||
19
public/locales/en/modules/indexer-manager.json
Normal file
19
public/locales/en/modules/indexer-manager.json
Normal file
@@ -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)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,20 @@
|
|||||||
"label": "Entity ID",
|
"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."
|
"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": {
|
"automationId": {
|
||||||
"label": "Optional automation ID",
|
"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."
|
"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": {
|
"displayName": {
|
||||||
"label": "Display name"
|
"label": "Display name"
|
||||||
|
},
|
||||||
|
"displayFriendlyName": {
|
||||||
|
"label": "Display friendly name",
|
||||||
|
"info": "Display friendly name from Home Assistant instead instead of display name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,10 @@
|
|||||||
"label": "Ordina per data di pubblicazione (ascendente)"
|
"label": "Ordina per data di pubblicazione (ascendente)"
|
||||||
},
|
},
|
||||||
"sortPostsWithoutPublishDateToTheTop": {
|
"sortPostsWithoutPublishDateToTheTop": {
|
||||||
"label": ""
|
"label": "Metti i post senza data di pubblicazione in alto"
|
||||||
},
|
},
|
||||||
"maximumAmountOfPosts": {
|
"maximumAmountOfPosts": {
|
||||||
"label": ""
|
"label": "Numero massimo di post"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"card": {
|
"card": {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"previous": "Iepriekšējais",
|
"previous": "Iepriekšējais",
|
||||||
"confirm": "Apstipriniet",
|
"confirm": "Apstipriniet",
|
||||||
"enabled": "Iespējots",
|
"enabled": "Iespējots",
|
||||||
"duplicate": "",
|
"duplicate": "Dublicēt",
|
||||||
"disabled": "Atspējots",
|
"disabled": "Atspējots",
|
||||||
"enableAll": "Iespējot visu",
|
"enableAll": "Iespējot visu",
|
||||||
"disableAll": "Atspējot visu",
|
"disableAll": "Atspējot visu",
|
||||||
@@ -54,5 +54,5 @@
|
|||||||
"height": "Augstums"
|
"height": "Augstums"
|
||||||
},
|
},
|
||||||
"public": "Publisks",
|
"public": "Publisks",
|
||||||
"restricted": ""
|
"restricted": "Ierobežots"
|
||||||
}
|
}
|
||||||
@@ -22,5 +22,5 @@
|
|||||||
"message": "Ir izveidota kategorija \"{{name}}\""
|
"message": "Ir izveidota kategorija \"{{name}}\""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"importFromDocker": ""
|
"importFromDocker": "Importēt no Docker"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,15 @@
|
|||||||
"label": "Neatgriezeniski dzēst",
|
"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ē."
|
"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": {
|
"rename": {
|
||||||
"label": "",
|
"label": "Pārdēvēt",
|
||||||
"modal": {
|
"modal": {
|
||||||
"title": "",
|
"title": "Pārdēvēt dēli {{name}}",
|
||||||
"fields": {
|
"fields": {
|
||||||
"name": {
|
"name": {
|
||||||
"label": "",
|
"label": "Jauns nosaukums",
|
||||||
"placeholder": ""
|
"placeholder": "Jauns dēļa nosaukums"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"roles": {
|
"roles": {
|
||||||
"all": "",
|
"all": "Viss",
|
||||||
"normal": "",
|
"normal": "Normāls",
|
||||||
"admin": "",
|
"admin": "Administrators",
|
||||||
"owner": ""
|
"owner": "Īpašnieks"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"metaTitle": "",
|
"metaTitle": "Lietotājs {{username}}",
|
||||||
"back": "",
|
"back": "Atgriezties uz lietotāju pārvaldību",
|
||||||
"sections": {
|
"sections": {
|
||||||
"general": {
|
"general": {
|
||||||
"title": "Vispārīgi",
|
"title": "Vispārīgi",
|
||||||
@@ -14,40 +14,40 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"title": "",
|
"title": "Drošība",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"password": {
|
"password": {
|
||||||
"label": ""
|
"label": "Jauna parole"
|
||||||
},
|
},
|
||||||
"terminateExistingSessions": {
|
"terminateExistingSessions": {
|
||||||
"label": "",
|
"label": "Pārtraukt esošās sesijas",
|
||||||
"description": ""
|
"description": "Piespiež lietotāju no jauna pieteikties savās ierīcēs"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"label": "Apstipriniet",
|
"label": "Apstipriniet",
|
||||||
"description": ""
|
"description": "Parole tiks atjaunināta. Šo darbību nevar atcelt."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"title": "",
|
"title": "Lomas",
|
||||||
"currentRole": "",
|
"currentRole": "Pašreizējā loma: ",
|
||||||
"badges": {
|
"badges": {
|
||||||
"owner": "",
|
"owner": "Īpašnieks",
|
||||||
"admin": "",
|
"admin": "Administrators",
|
||||||
"normal": ""
|
"normal": "Normāls"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deletion": {
|
"deletion": {
|
||||||
"title": "",
|
"title": "Konta dzēšana",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"confirmUsername": {
|
"confirmUsername": {
|
||||||
"label": "",
|
"label": "Apstiprināt lietotājvārdu",
|
||||||
"description": ""
|
"description": "Ierakstiet lietotājvārdu, lai apstiprinātu dzēšanu"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"label": "Neatgriezeniski dzēst",
|
"label": "Neatgriezeniski dzēst",
|
||||||
"description": ""
|
"description": "Es apzinos, ka šī darbība ir neatgriezeniska un visi konta dati tiks zaudēti."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"title": "Datuma un Laika logrīka iestatījumi",
|
"title": "Datuma un Laika logrīka iestatījumi",
|
||||||
"timezone": {
|
"timezone": {
|
||||||
"label": "",
|
"label": "Laika zona",
|
||||||
"info": ""
|
"info": "Izvēlieties savas laika zonas nosaukumu, atrodiet savējo šeit: "
|
||||||
},
|
},
|
||||||
"customTitle": {
|
"customTitle": {
|
||||||
"label": ""
|
"label": "Pilsētas nosaukums vai pielāgots nosaukums"
|
||||||
},
|
},
|
||||||
"display24HourFormat": {
|
"display24HourFormat": {
|
||||||
"label": "Rādīt pilnu laiku (24 stundu)"
|
"label": "Rādīt pilnu laiku (24 stundu)"
|
||||||
@@ -21,11 +21,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"titleState": {
|
"titleState": {
|
||||||
"label": "",
|
"label": "Pulksteņa nosaukums",
|
||||||
"info": "",
|
"info": "Pielāgotais nosaukums un laika zonas kods var tikt parādīts jūsu logrīkā.<br/>Varat arī rādīt tikai pilsētu, nerādīt nevienu,<br/>vai pat rādīt tikai laika joslu, gadījumā ja ir atlasīti abi, bet nav norādīts nosaukums.",
|
||||||
"data": {
|
"data": {
|
||||||
"both": "",
|
"both": "Pilsēta un Laika zona",
|
||||||
"city": "",
|
"city": "Tikai nosaukums",
|
||||||
"none": "Nekas"
|
"none": "Nekas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
"label": "Teksta līniju skava"
|
"label": "Teksta līniju skava"
|
||||||
},
|
},
|
||||||
"sortByPublishDateAscending": {
|
"sortByPublishDateAscending": {
|
||||||
"label": ""
|
"label": "Kārtot pēc publicēšanas datuma (augošā secībā)"
|
||||||
},
|
},
|
||||||
"sortPostsWithoutPublishDateToTheTop": {
|
"sortPostsWithoutPublishDateToTheTop": {
|
||||||
"label": ""
|
"label": "Ievietot ziņas bez publicēšanas datuma augšpusē"
|
||||||
},
|
},
|
||||||
"maximumAmountOfPosts": {
|
"maximumAmountOfPosts": {
|
||||||
"label": ""
|
"label": "Maksimālais ierakstu skaits"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"card": {
|
"card": {
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"entityNotFound": "",
|
"entityNotFound": "Vienība nav atrasta",
|
||||||
"descriptor": {
|
"descriptor": {
|
||||||
"name": "",
|
"name": "Home Assistant vienība",
|
||||||
"description": "",
|
"description": "Vienības pašreizējais stāvoklis pakalpojumā Home Assistant",
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "",
|
"title": "Vienības stāvoklis",
|
||||||
"entityId": {
|
"entityId": {
|
||||||
"label": "",
|
"label": "Vienības ID",
|
||||||
"info": ""
|
"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": {
|
"automationId": {
|
||||||
"label": "",
|
"label": "Izvēles automatizācijas ID",
|
||||||
"info": ""
|
"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": {
|
"displayName": {
|
||||||
"label": ""
|
"label": "Parādāmais nosaukums"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"descriptor": {
|
"descriptor": {
|
||||||
"name": "",
|
"name": "Home Assistant automatizācija",
|
||||||
"description": "",
|
"description": "Automatizācijas izpilde",
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "",
|
"title": "Automatizācijas izpilde",
|
||||||
"automationId": {
|
"automationId": {
|
||||||
"label": "",
|
"label": "Automatizācijas ID",
|
||||||
"info": ""
|
"info": "Jūsu unikālais automatizācijas ID. Vienmēr sāksies ar automation.XXXXX."
|
||||||
},
|
},
|
||||||
"displayName": {
|
"displayName": {
|
||||||
"label": ""
|
"label": "Parādāmais nosaukums"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,22 +41,22 @@
|
|||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"header": {
|
"header": {
|
||||||
"isCompleted": "",
|
"isCompleted": "Lejupielādē",
|
||||||
"name": "Nosaukums",
|
"name": "Nosaukums",
|
||||||
"dateAdded": "",
|
"dateAdded": "Pievienots",
|
||||||
"size": "Lielums",
|
"size": "Lielums",
|
||||||
"download": "Lejupielāde",
|
"download": "Lejupielāde",
|
||||||
"upload": "Augšupielāde",
|
"upload": "Augšupielāde",
|
||||||
"estimatedTimeOfArrival": "ETA",
|
"estimatedTimeOfArrival": "ETA",
|
||||||
"progress": "Progress",
|
"progress": "Progress",
|
||||||
"totalUploaded": "",
|
"totalUploaded": "Kopējā Augšupielāde",
|
||||||
"totalDownloaded": "",
|
"totalDownloaded": "Kopējā Lejupielāde",
|
||||||
"ratio": "",
|
"ratio": "Attiecība",
|
||||||
"seeds": "",
|
"seeds": "Devēji (savienoti)",
|
||||||
"peers": "",
|
"peers": "Ņēmēji (savienoti)",
|
||||||
"label": "",
|
"label": "Birka",
|
||||||
"state": "Stāvoklis",
|
"state": "Stāvoklis",
|
||||||
"stateMessage": ""
|
"stateMessage": "Statusa Ziņojums"
|
||||||
},
|
},
|
||||||
"item": {
|
"item": {
|
||||||
"text": "Pārvalda {{appName}}, {{ratio}} attiecība"
|
"text": "Pārvalda {{appName}}, {{ratio}} attiecība"
|
||||||
|
|||||||
@@ -19,26 +19,26 @@
|
|||||||
"label": "Fons"
|
"label": "Fons"
|
||||||
},
|
},
|
||||||
"backgroundImageAttachment": {
|
"backgroundImageAttachment": {
|
||||||
"label": "",
|
"label": "Fona attēla pielikums",
|
||||||
"options": {
|
"options": {
|
||||||
"fixed": "",
|
"fixed": "Fiksēts - fons paliek nemainīgā pozīcijā (ieteicams)",
|
||||||
"scroll": ""
|
"scroll": "Ritināšana - fons ritinās līdz ar kursora ritināšanu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"backgroundImageSize": {
|
"backgroundImageSize": {
|
||||||
"label": "",
|
"label": "Fona attēla izmērs",
|
||||||
"options": {
|
"options": {
|
||||||
"cover": "",
|
"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": ""
|
"contain": "Saturēt — mērogo attēlu pēc iespējas lielāku tā konteinerā, neapgriežot vai neizstiepjot attēlu."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"backgroundImageRepeat": {
|
"backgroundImageRepeat": {
|
||||||
"label": "",
|
"label": "Fona attēla pielikums",
|
||||||
"options": {
|
"options": {
|
||||||
"repeat": "",
|
"repeat": "Atkārtot — attēls tiek atkārtots tik daudz, cik nepieciešams, lai aptvertu visu fona laukumu.",
|
||||||
"no-repeat": "",
|
"no-repeat": "Bez atkārtojuma - attēls neatkārtojas un var neaizpildīt visu fona laukumu (ieteicams)",
|
||||||
"repeat-x": "",
|
"repeat-x": "Atkārtot X - tāpat kā \"Atkārtot\", bet tikai uz horizontālās ass.",
|
||||||
"repeat-y": ""
|
"repeat-y": "Atkārtot Y - tāpat kā \"Atkārtot\", bet tikai uz vertikālās ass."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"customCSS": {
|
"customCSS": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"title": "Docker",
|
"title": "Docker",
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"notConfigured": {
|
"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": {
|
"modals": {
|
||||||
|
|||||||
@@ -22,10 +22,10 @@
|
|||||||
"label": "Zoradiť podľa dátumu vydania (vzostupne)"
|
"label": "Zoradiť podľa dátumu vydania (vzostupne)"
|
||||||
},
|
},
|
||||||
"sortPostsWithoutPublishDateToTheTop": {
|
"sortPostsWithoutPublishDateToTheTop": {
|
||||||
"label": ""
|
"label": "Umiestnite príspevky bez dátumu uverejnenia na začiatok"
|
||||||
},
|
},
|
||||||
"maximumAmountOfPosts": {
|
"maximumAmountOfPosts": {
|
||||||
"label": ""
|
"label": "Maximálny počet príspevkov"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"card": {
|
"card": {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
"disabled": "Pasif",
|
"disabled": "Pasif",
|
||||||
"enableAll": "Tümünü etkinleştir",
|
"enableAll": "Tümünü etkinleştir",
|
||||||
"disableAll": "Tümünü pasifleştir",
|
"disableAll": "Tümünü pasifleştir",
|
||||||
"version": "Versiyon",
|
"version": "Sürüm",
|
||||||
"changePosition": "Pozisyon değiştir",
|
"changePosition": "Pozisyonu değiştir",
|
||||||
"remove": "Kaldır",
|
"remove": "Kaldır",
|
||||||
"removeConfirm": "{{item}}'i kaldırmak istediğinizden emin misiniz?",
|
"removeConfirm": "{{item}}'i kaldırmak istediğinizden emin misiniz?",
|
||||||
"createItem": "+ yeni {{item}}",
|
"createItem": "+ yeni {{item}}",
|
||||||
|
|||||||
@@ -163,6 +163,11 @@ export const availableIntegrations = [
|
|||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png',
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png',
|
||||||
label: 'Readarr',
|
label: 'Readarr',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'prowlarr',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/prowlarr.png',
|
||||||
|
label: 'Prowlarr',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'jellyfin',
|
value: 'jellyfin',
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png',
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png',
|
||||||
@@ -186,6 +191,6 @@ export const availableIntegrations = [
|
|||||||
{
|
{
|
||||||
value: 'homeAssistant',
|
value: 'homeAssistant',
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png',
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png',
|
||||||
label: 'Home Assistant'
|
label: 'Home Assistant',
|
||||||
}
|
},
|
||||||
] as const satisfies Readonly<SelectItem[]>;
|
] as const satisfies Readonly<SelectItem[]>;
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ import { createBoardSchemaValidation } from '~/validations/boards';
|
|||||||
|
|
||||||
export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
|
export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
|
||||||
const { t } = useTranslation('manage/boards');
|
const { t } = useTranslation('manage/boards');
|
||||||
const utils = api.useContext();
|
const utils = api.useUtils();
|
||||||
const { isLoading, mutate } = api.config.save.useMutation({
|
const { isLoading, mutate } = api.config.save.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.boards.all.invalidate();
|
await utils.boards.all.invalidate();
|
||||||
modals.close(id);
|
modals.close(id);
|
||||||
},
|
},
|
||||||
|
onError: async (error) => {
|
||||||
|
form.setFieldError('name', error.message);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { i18nZodResolver } = useI18nZodResolver();
|
const { i18nZodResolver } = useI18nZodResolver();
|
||||||
@@ -31,6 +34,7 @@ export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
|
|||||||
mutate({
|
mutate({
|
||||||
name: form.values.name,
|
name: form.values.name,
|
||||||
config: fallbackConfig,
|
config: fallbackConfig,
|
||||||
|
create: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
umami.track('Create new board')
|
umami.track('Create new board');
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
variant="light"
|
variant="light"
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export const ReviewInputStep = ({ values, prevStep, nextStep }: ReviewInputStepP
|
|||||||
password: values.security.password,
|
password: values.security.password,
|
||||||
email: values.account.eMail === '' ? undefined : values.account.eMail,
|
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}
|
loading={isLoading}
|
||||||
rightIcon={<IconCheck size="1rem" />}
|
rightIcon={<IconCheck size="1rem" />}
|
||||||
|
|||||||
@@ -57,9 +57,12 @@ export const StepCreateAccount = ({
|
|||||||
Create your administrator account
|
Create your administrator account
|
||||||
</Title>
|
</Title>
|
||||||
<Text>
|
<Text>
|
||||||
Your administrator account <b>must be secure</b>, that's why we have so many rules surrounding it.
|
Your administrator account <b>must be secure</b>, that's why we have so many rules
|
||||||
<br/>Try not to make it adminadmin this time...
|
surrounding it.
|
||||||
<br/>Note: these password requirements <b>are not forced</b>, they are just recommendations.
|
<br />
|
||||||
|
Try not to make it adminadmin this time...
|
||||||
|
<br />
|
||||||
|
Note: these password requirements <b>are not forced</b>, they are just recommendations.
|
||||||
</Text>
|
</Text>
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
|||||||
60
src/env.js
60
src/env.js
@@ -1,6 +1,14 @@
|
|||||||
const { z } = require('zod');
|
const { z } = require('zod');
|
||||||
const { createEnv } = require('@t3-oss/env-nextjs');
|
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
|
const portSchema = z
|
||||||
.string()
|
.string()
|
||||||
.regex(/\d*/)
|
.regex(/\d*/)
|
||||||
@@ -8,6 +16,8 @@ const portSchema = z
|
|||||||
.optional();
|
.optional();
|
||||||
const envSchema = z.enum(['development', 'test', 'production']);
|
const envSchema = z.enum(['development', 'test', 'production']);
|
||||||
|
|
||||||
|
const authProviders = process.env.AUTH_PROVIDER?.replaceAll(' ', '').split(',') || ['credentials'];
|
||||||
|
|
||||||
const env = createEnv({
|
const env = createEnv({
|
||||||
/**
|
/**
|
||||||
* Specify your server-side environment variables schema here. This way you can ensure the app
|
* 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,
|
DOCKER_PORT: portSchema,
|
||||||
DEMO_MODE: z.string().optional(),
|
DEMO_MODE: z.string().optional(),
|
||||||
HOSTNAME: 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_PORT: process.env.PORT,
|
||||||
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
|
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
|
||||||
HOSTNAME: process.env.HOSTNAME,
|
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,
|
DEMO_MODE: process.env.DEMO_MODE,
|
||||||
},
|
},
|
||||||
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const skippedUrls = [
|
|||||||
'/favicon.ico',
|
'/favicon.ico',
|
||||||
'/404',
|
'/404',
|
||||||
'/pages/_app',
|
'/pages/_app',
|
||||||
|
'/auth/login',
|
||||||
'/imgs/',
|
'/imgs/',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -29,12 +30,15 @@ export async function middleware(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do not redirect if there are users in the database
|
// Do not redirect if there are users in the database
|
||||||
if (cachedUserCount > 0) {
|
if (cachedUserCount > 0 || !(await shouldRedirectToOnboard())) {
|
||||||
return NextResponse.next();
|
// redirect to login if not logged in
|
||||||
}
|
// not working, should work in next-auth 5
|
||||||
|
// @see https://github.com/nextauthjs/next-auth/pull/7443
|
||||||
|
|
||||||
// Do not redirect if there are users in the database
|
// const session = await getServerSession();
|
||||||
if (!(await shouldRedirectToOnboard())) {
|
// if (!session?.user) {
|
||||||
|
// return NextResponse.redirect(getUrl(req) + '/auth/login')
|
||||||
|
// }
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,8 +125,7 @@ export default function AuthInvitePage() {
|
|||||||
withAsterisk
|
withAsterisk
|
||||||
{...form.getInputProps('password')}
|
{...form.getInputProps('password')}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card>
|
||||||
>
|
|
||||||
<PasswordRequirements value={form.values.password} />
|
<PasswordRequirements value={form.values.password} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -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 { useForm } from '@mantine/form';
|
||||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ThemeSchemeToggle } from '~/components/ThemeSchemeToggle/ThemeSchemeToggle';
|
import { ThemeSchemeToggle } from '~/components/ThemeSchemeToggle/ThemeSchemeToggle';
|
||||||
import { FloatingBackground } from '~/components/layout/Background/FloatingBackground';
|
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 { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
import { signInSchema } from '~/validations/user';
|
import { signInSchema } from '~/validations/user';
|
||||||
|
|
||||||
|
const signInSchemaWithProvider = signInSchema.extend({ provider: z.string() });
|
||||||
|
|
||||||
export default function LoginPage({
|
export default function LoginPage({
|
||||||
redirectAfterLogin,
|
redirectAfterLogin,
|
||||||
|
providers,
|
||||||
|
oidcProviderName,
|
||||||
|
oidcAutoLogin,
|
||||||
isDemo,
|
isDemo,
|
||||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||||
const { t } = useTranslation('authentication/login');
|
const { t } = useTranslation('authentication/login');
|
||||||
@@ -27,16 +43,18 @@ export default function LoginPage({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof signInSchema>>({
|
const hasCredentialsInput = providers.includes('credentials') || providers.includes('ldap');
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof signInSchemaWithProvider>>({
|
||||||
validateInputOnChange: true,
|
validateInputOnChange: true,
|
||||||
validateInputOnBlur: true,
|
validateInputOnBlur: true,
|
||||||
validate: i18nZodResolver(signInSchema),
|
validate: i18nZodResolver(signInSchemaWithProvider),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (values: z.infer<typeof signInSchema>) => {
|
const handleSubmit = (values: z.infer<typeof signInSchemaWithProvider>) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setIsError(false);
|
setIsError(false);
|
||||||
signIn('credentials', {
|
signIn(values.provider, {
|
||||||
redirect: false,
|
redirect: false,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
@@ -51,6 +69,10 @@ export default function LoginPage({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (oidcAutoLogin) signIn('oidc');
|
||||||
|
}, [oidcAutoLogin]);
|
||||||
|
|
||||||
const metaTitle = `${t('metaTitle')} • Homarr`;
|
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,7 +80,6 @@ export default function LoginPage({
|
|||||||
<Head>
|
<Head>
|
||||||
<title>{metaTitle}</title>
|
<title>{metaTitle}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
||||||
<FloatingBackground />
|
<FloatingBackground />
|
||||||
<ThemeSchemeToggle pos="absolute" top={20} right={20} />
|
<ThemeSchemeToggle pos="absolute" top={20} right={20} />
|
||||||
@@ -83,51 +104,94 @@ export default function LoginPage({
|
|||||||
<b>demodemo</b>
|
<b>demodemo</b>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={450}>
|
{oidcAutoLogin ? (
|
||||||
<Title style={{ whiteSpace: 'nowrap' }} align="center" weight={900}>
|
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={450}>
|
||||||
{t('title')}
|
<Text size="lg" align="center" m="md">
|
||||||
</Title>
|
Signing in with OIDC provider
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={450}>
|
||||||
|
<Title style={{ whiteSpace: 'nowrap' }} align="center" weight={900}>
|
||||||
|
{t('title')}
|
||||||
|
</Title>
|
||||||
|
|
||||||
<Text color="dimmed" size="sm" align="center" mt={5} mb="md">
|
<Text color="dimmed" size="sm" align="center" mt={5} mb="md">
|
||||||
{t('text')}
|
{t('text')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{isError && (
|
{isError && (
|
||||||
<Alert icon={<IconAlertTriangle size="1rem" />} color="red">
|
<Alert icon={<IconAlertTriangle size="1rem" />} color="red">
|
||||||
{t('alert')}
|
{t('alert')}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
{hasCredentialsInput && (
|
||||||
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
|
<Stack>
|
||||||
|
<TextInput
|
||||||
|
variant="filled"
|
||||||
|
label={t('form.fields.username.label')}
|
||||||
|
autoComplete="homarr-username"
|
||||||
|
withAsterisk
|
||||||
|
{...form.getInputProps('name')}
|
||||||
|
/>
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<PasswordInput
|
||||||
<Stack>
|
variant="filled"
|
||||||
<TextInput
|
label={t('form.fields.password.label')}
|
||||||
variant="filled"
|
autoComplete="homarr-password"
|
||||||
label={t('form.fields.username.label')}
|
withAsterisk
|
||||||
autoComplete="homarr-username"
|
{...form.getInputProps('password')}
|
||||||
withAsterisk
|
/>
|
||||||
{...form.getInputProps('name')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PasswordInput
|
{providers.includes('credentials') && (
|
||||||
variant="filled"
|
<Button
|
||||||
label={t('form.fields.password.label')}
|
mt="xs"
|
||||||
autoComplete="homarr-password"
|
variant="light"
|
||||||
withAsterisk
|
fullWidth
|
||||||
{...form.getInputProps('password')}
|
type="submit"
|
||||||
/>
|
disabled={isLoading && form.values.provider != 'credentials'}
|
||||||
|
loading={isLoading && form.values.provider == 'credentials'}
|
||||||
|
name="credentials"
|
||||||
|
onClick={() => form.setFieldValue('provider', 'credentials')}
|
||||||
|
>
|
||||||
|
{t('form.buttons.submit')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button mt="xs" variant="light" fullWidth type="submit" loading={isLoading}>
|
{providers.includes('ldap') && (
|
||||||
{t('form.buttons.submit')}
|
<Button
|
||||||
|
mt="xs"
|
||||||
|
variant="light"
|
||||||
|
fullWidth
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading && form.values.provider != 'ldap'}
|
||||||
|
loading={isLoading && form.values.provider == 'ldap'}
|
||||||
|
name="ldap"
|
||||||
|
onClick={() => form.setFieldValue('provider', 'ldap')}
|
||||||
|
>
|
||||||
|
{t('form.buttons.submit')} - LDAP
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{redirectAfterLogin && (
|
||||||
|
<Text color="dimmed" align="center" size="xs">
|
||||||
|
{t('form.afterLoginRedirection', { url: redirectAfterLogin })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
{hasCredentialsInput && providers.includes('oidc') && (
|
||||||
|
<Divider label="OIDC" labelPosition="center" mt="xl" mb="md" />
|
||||||
|
)}
|
||||||
|
{providers.includes('oidc') && (
|
||||||
|
<Button mt="xs" variant="light" fullWidth onClick={() => signIn('oidc')}>
|
||||||
|
{t('form.buttons.submit')} - {oidcProviderName}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
{redirectAfterLogin && (
|
</Card>
|
||||||
<Text color="dimmed" align="center" size="xs">
|
)}
|
||||||
{t('form.afterLoginRedirection', { url: redirectAfterLogin })}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
@@ -136,7 +200,12 @@ export default function LoginPage({
|
|||||||
|
|
||||||
const regexExp = /^\/{1}[A-Za-z\/]*$/;
|
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 session = await getServerAuthSession({ req, res });
|
||||||
|
|
||||||
const zodResult = await z
|
const zodResult = await z
|
||||||
@@ -159,6 +228,9 @@ export const getServerSideProps: GetServerSideProps = async ({ locale, req, res,
|
|||||||
props: {
|
props: {
|
||||||
...(await getServerSideTranslations(['authentication/login'], locale, req, res)),
|
...(await getServerSideTranslations(['authentication/login'], locale, req, res)),
|
||||||
redirectAfterLogin,
|
redirectAfterLogin,
|
||||||
|
providers: env.AUTH_PROVIDER,
|
||||||
|
oidcProviderName: env.AUTH_OIDC_CLIENT_NAME || null,
|
||||||
|
oidcAutoLogin: env.AUTH_OIDC_AUTO_LOGIN || null,
|
||||||
isDemo,
|
isDemo,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createTRPCRouter } from '~/server/api/trpc';
|
import { createTRPCRouter } from '~/server/api/trpc';
|
||||||
|
|
||||||
import { appRouter } from './routers/app';
|
import { appRouter } from './routers/app';
|
||||||
import { boardRouter } from './routers/board';
|
import { boardRouter } from './routers/board';
|
||||||
import { calendarRouter } from './routers/calendar';
|
import { calendarRouter } from './routers/calendar';
|
||||||
@@ -8,6 +9,7 @@ import { dnsHoleRouter } from './routers/dns-hole/router';
|
|||||||
import { dockerRouter } from './routers/docker/router';
|
import { dockerRouter } from './routers/docker/router';
|
||||||
import { downloadRouter } from './routers/download';
|
import { downloadRouter } from './routers/download';
|
||||||
import { iconRouter } from './routers/icon';
|
import { iconRouter } from './routers/icon';
|
||||||
|
import { indexerManagerRouter } from './routers/indexer-manager';
|
||||||
import { inviteRouter } from './routers/invite/invite-router';
|
import { inviteRouter } from './routers/invite/invite-router';
|
||||||
import { mediaRequestsRouter } from './routers/media-request';
|
import { mediaRequestsRouter } from './routers/media-request';
|
||||||
import { mediaServerRouter } from './routers/media-server';
|
import { mediaServerRouter } from './routers/media-server';
|
||||||
@@ -30,6 +32,7 @@ export const rootRouter = createTRPCRouter({
|
|||||||
rss: rssRouter,
|
rss: rssRouter,
|
||||||
user: userRouter,
|
user: userRouter,
|
||||||
calendar: calendarRouter,
|
calendar: calendarRouter,
|
||||||
|
indexerManager: indexerManagerRouter,
|
||||||
config: configRouter,
|
config: configRouter,
|
||||||
dashDot: dashDotRouter,
|
dashDot: dashDotRouter,
|
||||||
dnsHole: dnsHoleRouter,
|
dnsHole: dnsHoleRouter,
|
||||||
@@ -45,7 +48,7 @@ export const rootRouter = createTRPCRouter({
|
|||||||
boards: boardRouter,
|
boards: boardRouter,
|
||||||
password: passwordRouter,
|
password: passwordRouter,
|
||||||
notebook: notebookRouter,
|
notebook: notebookRouter,
|
||||||
smartHomeEntityState: smartHomeEntityStateRouter
|
smartHomeEntityState: smartHomeEntityStateRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ export const configRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: configNameSchema,
|
name: configNameSchema,
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.output(z.object({ message: z.string() }))
|
.output(z.object({ message: z.string() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
if (input.name.toLowerCase() === 'default') {
|
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({
|
throw new TRPCError({
|
||||||
code: 'FORBIDDEN',
|
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 the target is not in the list of files, return an error
|
||||||
if (!matchedFile) {
|
if (!matchedFile) {
|
||||||
Consola.error(
|
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({
|
throw new TRPCError({
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
@@ -64,9 +64,13 @@ export const configRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
name: configNameSchema,
|
name: configNameSchema,
|
||||||
config: z.custom<ConfigType>((x) => !!x && typeof x === 'object'),
|
config: z.custom<ConfigType>((x) => !!x && typeof x === 'object'),
|
||||||
}),
|
create: z.boolean().optional(),
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.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.`);
|
Consola.info(`Saving updated configuration of '${input.name}' config.`);
|
||||||
|
|
||||||
const previousConfig = getConfig(input.name);
|
const previousConfig = getConfig(input.name);
|
||||||
@@ -96,16 +100,16 @@ export const configRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previousApp = previousConfig.apps.find(
|
const previousApp = previousConfig.apps.find(
|
||||||
(previousApp) => previousApp.id === app.id,
|
(previousApp) => previousApp.id === app.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const previousProperty = previousApp?.integration?.properties.find(
|
const previousProperty = previousApp?.integration?.properties.find(
|
||||||
(previousProperty) => previousProperty.field === property.field,
|
(previousProperty) => previousProperty.field === property.field
|
||||||
);
|
);
|
||||||
|
|
||||||
if (property.value !== undefined && property.value !== null) {
|
if (property.value !== undefined && property.value !== null) {
|
||||||
Consola.info(
|
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 {
|
return {
|
||||||
field: property.field,
|
field: property.field,
|
||||||
@@ -168,13 +172,14 @@ export const configRouter = createTRPCRouter({
|
|||||||
path: '/configs/byName',
|
path: '/configs/byName',
|
||||||
tags: ['config'],
|
tags: ['config'],
|
||||||
deprecated: true,
|
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(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: configNameSchema,
|
name: configNameSchema,
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.output(z.custom<ConfigType>())
|
.output(z.custom<ConfigType>())
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
|||||||
102
src/server/api/routers/indexer-manager.ts
Normal file
102
src/server/api/routers/indexer-manager.ts
Normal file
@@ -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;
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -1,57 +1,17 @@
|
|||||||
import { DrizzleAdapter } from '@auth/drizzle-adapter';
|
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import Consola from 'consola';
|
|
||||||
import Cookies from 'cookies';
|
import Cookies from 'cookies';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { type GetServerSidePropsContext, type NextApiRequest, type NextApiResponse } from 'next';
|
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 { Adapter } from 'next-auth/adapters';
|
||||||
import { decode, encode } from 'next-auth/jwt';
|
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 EmptyNextAuthProvider from '~/utils/empty-provider';
|
||||||
import { fromDate, generateSessionToken } from '~/utils/session';
|
import { fromDate, generateSessionToken } from '~/utils/session';
|
||||||
import { colorSchemeParser, signInSchema } from '~/validations/user';
|
import { colorSchemeParser } from '~/validations/user';
|
||||||
|
|
||||||
import { db } from './db';
|
import { db } from './db';
|
||||||
import { users } from './db/schema';
|
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
|
const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +23,9 @@ export const constructAuthOptions = (
|
|||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
): NextAuthOptions => ({
|
): NextAuthOptions => ({
|
||||||
|
events: {
|
||||||
|
createUser: onCreateUser,
|
||||||
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async session({ session, user }) {
|
async session({ session, user }) {
|
||||||
if (session.user) {
|
if (session.user) {
|
||||||
@@ -133,58 +96,7 @@ export const constructAuthOptions = (
|
|||||||
error: '/auth/login',
|
error: '/auth/login',
|
||||||
},
|
},
|
||||||
adapter: adapter as Adapter,
|
adapter: adapter as Adapter,
|
||||||
providers: [
|
providers: [...providers, EmptyNextAuthProvider()],
|
||||||
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(),
|
|
||||||
],
|
|
||||||
jwt: {
|
jwt: {
|
||||||
async encode(params) {
|
async encode(params) {
|
||||||
if (!isCredentialsRequest(req)) {
|
if (!isCredentialsRequest(req)) {
|
||||||
@@ -207,10 +119,12 @@ export const constructAuthOptions = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isCredentialsRequest = (req: NextApiRequest): boolean => {
|
const isCredentialsRequest = (req: NextApiRequest): boolean => {
|
||||||
const nextAuthQueryParams = req.query.nextauth as ['callback', 'credentials'];
|
const nextAuthQueryParams = req.query.nextauth as string[];
|
||||||
return (
|
return (
|
||||||
nextAuthQueryParams.includes('callback') &&
|
nextAuthQueryParams.includes('callback') &&
|
||||||
nextAuthQueryParams.includes('credentials') &&
|
(nextAuthQueryParams.includes('credentials') ||
|
||||||
|
nextAuthQueryParams.includes('ldap') ||
|
||||||
|
nextAuthQueryParams.includes('oidc')) &&
|
||||||
req.method === 'POST'
|
req.method === 'POST'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { InferSelectModel, relations } from 'drizzle-orm';
|
|||||||
import { index, int, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
import { index, int, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||||
import { type AdapterAccount } from 'next-auth/adapters';
|
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(),
|
id: text('id').notNull().primaryKey(),
|
||||||
name: text('name'),
|
name: text('name'),
|
||||||
email: text('email'),
|
email: text('email'),
|
||||||
@@ -12,7 +14,9 @@ export const users = sqliteTable('user', {
|
|||||||
salt: text('salt'),
|
salt: text('salt'),
|
||||||
isAdmin: int('is_admin', { mode: 'boolean' }).notNull().default(false),
|
isAdmin: int('is_admin', { mode: 'boolean' }).notNull().default(false),
|
||||||
isOwner: int('is_owner', { mode: 'boolean' }).notNull().default(false),
|
isOwner: int('is_owner', { mode: 'boolean' }).notNull().default(false),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export const users = sqliteTable('user', _users);
|
||||||
|
|
||||||
export const accounts = sqliteTable(
|
export const accounts = sqliteTable(
|
||||||
'account',
|
'account',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const boardNamespaces = [
|
|||||||
'modules/dashdot',
|
'modules/dashdot',
|
||||||
'modules/overseerr',
|
'modules/overseerr',
|
||||||
'modules/media-server',
|
'modules/media-server',
|
||||||
|
'modules/indexer-manager',
|
||||||
'modules/common-media-cards',
|
'modules/common-media-cards',
|
||||||
'modules/video-stream',
|
'modules/video-stream',
|
||||||
'modules/media-requests-list',
|
'modules/media-requests-list',
|
||||||
@@ -44,7 +45,7 @@ export const manageNamespaces = [
|
|||||||
'manage/users',
|
'manage/users',
|
||||||
'manage/users/invites',
|
'manage/users/invites',
|
||||||
'manage/users/create',
|
'manage/users/create',
|
||||||
'manage/users/edit'
|
'manage/users/edit',
|
||||||
];
|
];
|
||||||
export const loginNamespaces = ['authentication/login'];
|
export const loginNamespaces = ['authentication/login'];
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Icon, IconKey, IconPassword, IconUser } from '@tabler/icons-react';
|
import { Icon, IconKey, IconPassword, IconUser } from '@tabler/icons-react';
|
||||||
|
|
||||||
import { Property } from 'csstype';
|
import { Property } from 'csstype';
|
||||||
|
|
||||||
import { TileBaseType } from './tile';
|
import { TileBaseType } from './tile';
|
||||||
@@ -46,6 +45,7 @@ export type IntegrationType =
|
|||||||
| 'radarr'
|
| 'radarr'
|
||||||
| 'sonarr'
|
| 'sonarr'
|
||||||
| 'lidarr'
|
| 'lidarr'
|
||||||
|
| 'prowlarr'
|
||||||
| 'sabnzbd'
|
| 'sabnzbd'
|
||||||
| 'jellyseerr'
|
| 'jellyseerr'
|
||||||
| 'overseerr'
|
| 'overseerr'
|
||||||
@@ -87,6 +87,7 @@ export const integrationFieldProperties: {
|
|||||||
lidarr: ['apiKey'],
|
lidarr: ['apiKey'],
|
||||||
radarr: ['apiKey'],
|
radarr: ['apiKey'],
|
||||||
sonarr: ['apiKey'],
|
sonarr: ['apiKey'],
|
||||||
|
prowlarr: ['apiKey'],
|
||||||
sabnzbd: ['apiKey'],
|
sabnzbd: ['apiKey'],
|
||||||
readarr: ['apiKey'],
|
readarr: ['apiKey'],
|
||||||
overseerr: ['apiKey'],
|
overseerr: ['apiKey'],
|
||||||
@@ -99,7 +100,7 @@ export const integrationFieldProperties: {
|
|||||||
plex: ['apiKey'],
|
plex: ['apiKey'],
|
||||||
pihole: ['apiKey'],
|
pihole: ['apiKey'],
|
||||||
adGuardHome: ['username', 'password'],
|
adGuardHome: ['username', 'password'],
|
||||||
homeAssistant: ['apiKey']
|
homeAssistant: ['apiKey'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IntegrationFieldDefinitionType = {
|
export type IntegrationFieldDefinitionType = {
|
||||||
|
|||||||
166
src/utils/auth/adapter.ts
Normal file
166
src/utils/auth/adapter.ts
Normal file
@@ -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<typeof createTables>;
|
||||||
|
|
||||||
|
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<typeof BaseSQLiteDatabase>,
|
||||||
|
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);
|
||||||
56
src/utils/auth/credentials.ts
Normal file
56
src/utils/auth/credentials.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
46
src/utils/auth/index.ts
Normal file
46
src/utils/auth/index.ts
Normal file
@@ -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<any>)[] = [];
|
||||||
|
|
||||||
|
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);
|
||||||
161
src/utils/auth/ldap.ts
Normal file
161
src/utils/auth/ldap.ts
Normal file
@@ -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<ldap.SearchOptions, 'attributes'> & {
|
||||||
|
attributes?: Attributes;
|
||||||
|
arrayAttributes?: ArrayAttributes;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchResultIndex<Attributes extends AttributeConstraint> = 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<Attributes>, SearchResultIndex<ArrayAttributes>>,
|
||||||
|
string
|
||||||
|
> &
|
||||||
|
Record<SearchResultIndex<ArrayAttributes>, string[]>;
|
||||||
|
|
||||||
|
const ldapLogin = (username: string, password: string) =>
|
||||||
|
new Promise<ldap.Client>((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<Attributes, ArrayAttributes>
|
||||||
|
) =>
|
||||||
|
new Promise<SearchResult<Attributes, ArrayAttributes>[]>((resolve, reject) => {
|
||||||
|
client.search(base, options as ldap.SearchOptions, (err, res) => {
|
||||||
|
const results: SearchResult<Attributes, ArrayAttributes>[] = [];
|
||||||
|
res.on('error', (err) => {
|
||||||
|
reject('error: ' + err.message);
|
||||||
|
});
|
||||||
|
res.on('searchEntry', (entry) => {
|
||||||
|
results.push(
|
||||||
|
entry.pojo.attributes.reduce<Record<string, string | string[]>>(
|
||||||
|
(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<Attributes, ArrayAttributes>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
51
src/utils/auth/oidc.ts
Normal file
51
src/utils/auth/oidc.ts
Normal file
@@ -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<Profile> = {
|
||||||
|
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;
|
||||||
@@ -87,7 +87,7 @@ const definition = defineWidget({
|
|||||||
return t('item.validation.length', { shortest: '1', longest: '100' });
|
return t('item.validation.length', { shortest: '1', longest: '100' });
|
||||||
},
|
},
|
||||||
href: (value) => {
|
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' });
|
return t('item.validation.length', { shortest: '1', longest: '200' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import dnsHoleControls from './dnshole/DnsHoleControls';
|
|||||||
import dnsHoleSummary from './dnshole/DnsHoleSummary';
|
import dnsHoleSummary from './dnshole/DnsHoleSummary';
|
||||||
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
|
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
|
||||||
import iframe from './iframe/IFrameTile';
|
import iframe from './iframe/IFrameTile';
|
||||||
|
import indexerManager from './indexer-manager/IndexerManagerTile';
|
||||||
import mediaRequestsList from './media-requests/MediaRequestListTile';
|
import mediaRequestsList from './media-requests/MediaRequestListTile';
|
||||||
import mediaRequestsStats from './media-requests/MediaRequestStatsTile';
|
import mediaRequestsStats from './media-requests/MediaRequestStatsTile';
|
||||||
import mediaServer from './media-server/MediaServerTile';
|
import mediaServer from './media-server/MediaServerTile';
|
||||||
@@ -20,6 +21,7 @@ import weather from './weather/WeatherTile';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
calendar,
|
calendar,
|
||||||
|
'indexer-manager': indexerManager,
|
||||||
dashdot,
|
dashdot,
|
||||||
usenet,
|
usenet,
|
||||||
weather,
|
weather,
|
||||||
|
|||||||
98
src/widgets/indexer-manager/IndexerManagerTile.tsx
Normal file
98
src/widgets/indexer-manager/IndexerManagerTile.tsx
Normal file
@@ -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 <WidgetLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex h="100%" gap={0} direction="column">
|
||||||
|
<Text mt={2}>{t('indexersStatus.title')}</Text>
|
||||||
|
<Card py={5} px={10} radius="md" withBorder style={{ flex: '1' }}>
|
||||||
|
<ScrollArea h="100%">
|
||||||
|
{indexersData.map((indexer: any) => (
|
||||||
|
<Group key={indexer.id} position="apart">
|
||||||
|
<Text color="dimmed" align="center" size="xs">
|
||||||
|
{indexer.name}
|
||||||
|
</Text>
|
||||||
|
{!statusesData.find((status: any) => indexer.id === status.indexerId) &&
|
||||||
|
indexer.enable ? (
|
||||||
|
<IconCircleCheck color="#2ecc71" />
|
||||||
|
) : (
|
||||||
|
<IconCircleX color="#d9534f" />
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</ScrollArea>
|
||||||
|
</Card>
|
||||||
|
{sessionData && (
|
||||||
|
<Button
|
||||||
|
mt={5}
|
||||||
|
radius="md"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => {
|
||||||
|
testAllAsync({ configName: configName! });
|
||||||
|
}}
|
||||||
|
loading={testAllLoading}
|
||||||
|
loaderPosition="right"
|
||||||
|
rightIcon={<IconTestPipe size={20} />}
|
||||||
|
>
|
||||||
|
{t('indexersStatus.testAllButton')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definition;
|
||||||
@@ -95,6 +95,13 @@ export function Editor({ widget }: { widget: INotebookWidget }) {
|
|||||||
validate(url) {
|
validate(url) {
|
||||||
return /^https?:\/\//.test(url);
|
return /^https?:\/\//.test(url);
|
||||||
},
|
},
|
||||||
|
}).extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
target: { default: null }
|
||||||
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
StarterKit,
|
StarterKit,
|
||||||
Table.configure({
|
Table.configure({
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ const definition = defineWidget({
|
|||||||
defaultValue: 'sun.sun',
|
defaultValue: 'sun.sun',
|
||||||
info: true,
|
info: true,
|
||||||
},
|
},
|
||||||
|
appendUnit: {
|
||||||
|
type: 'switch',
|
||||||
|
defaultValue: false,
|
||||||
|
info: true,
|
||||||
|
},
|
||||||
automationId: {
|
automationId: {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
info: true,
|
info: true,
|
||||||
@@ -25,6 +30,11 @@ const definition = defineWidget({
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
defaultValue: 'Sun',
|
defaultValue: 'Sun',
|
||||||
},
|
},
|
||||||
|
displayFriendlyName: {
|
||||||
|
type: 'switch',
|
||||||
|
defaultValue: false,
|
||||||
|
info: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
gridstack: {
|
gridstack: {
|
||||||
minWidth: 1,
|
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({
|
const { mutateAsync: mutateTriggerAutomationAsync } = api.smartHomeEntityState.triggerAutomation.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void utils.smartHomeEntityState.invalidate();
|
void utils.smartHomeEntityState.invalidate();
|
||||||
@@ -101,6 +119,7 @@ function EntityStateTile({ widget }: SmartHomeEntityStateWidgetProps) {
|
|||||||
dataComponent = (
|
dataComponent = (
|
||||||
<Text align="center">
|
<Text align="center">
|
||||||
{data?.state}
|
{data?.state}
|
||||||
|
{attribute}
|
||||||
{isLoading && <Loader ml="xs" size={10} />}
|
{isLoading && <Loader ml="xs" size={10} />}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -118,7 +137,7 @@ function EntityStateTile({ widget }: SmartHomeEntityStateWidgetProps) {
|
|||||||
w="100%">
|
w="100%">
|
||||||
<Stack align="center" spacing={3}>
|
<Stack align="center" spacing={3}>
|
||||||
<Text align="center" weight="bold" size="lg">
|
<Text align="center" weight="bold" size="lg">
|
||||||
{widget.properties.displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
{dataComponent}
|
{dataComponent}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ describe('login page', () => {
|
|||||||
redirectAfterLogin: null,
|
redirectAfterLogin: null,
|
||||||
isDemo: false,
|
isDemo: false,
|
||||||
_i18Next: 'hello',
|
_i18Next: 'hello',
|
||||||
|
oidcAutoLogin: null,
|
||||||
|
oidcProviderName: null,
|
||||||
|
providers: undefined
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,6 +78,9 @@ describe('login page', () => {
|
|||||||
redirectAfterLogin: '/manage/users/create',
|
redirectAfterLogin: '/manage/users/create',
|
||||||
isDemo: false,
|
isDemo: false,
|
||||||
_i18Next: 'hello',
|
_i18Next: 'hello',
|
||||||
|
oidcAutoLogin: null,
|
||||||
|
oidcProviderName: null,
|
||||||
|
providers: undefined
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,6 +118,9 @@ describe('login page', () => {
|
|||||||
redirectAfterLogin: null,
|
redirectAfterLogin: null,
|
||||||
isDemo: false,
|
isDemo: false,
|
||||||
_i18Next: 'hello',
|
_i18Next: 'hello',
|
||||||
|
oidcAutoLogin: null,
|
||||||
|
oidcProviderName: null,
|
||||||
|
providers: undefined
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "es2017",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
|
|||||||
299
yarn.lock
299
yarn.lock
@@ -22,34 +22,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@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
|
version: 7.22.13
|
||||||
resolution: "@babel/code-frame@npm:7.22.13"
|
resolution: "@babel/code-frame@npm:7.22.13"
|
||||||
@@ -1033,6 +1005,95 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@mantine/core@npm:^6.0.0":
|
||||||
version: 6.0.21
|
version: 6.0.21
|
||||||
resolution: "@mantine/core@npm:6.0.21"
|
resolution: "@mantine/core@npm:6.0.21"
|
||||||
@@ -1504,7 +1565,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@panva/hkdf@npm:^1.0.2, @panva/hkdf@npm:^1.1.1":
|
"@panva/hkdf@npm:^1.0.2":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "@panva/hkdf@npm:1.1.1"
|
resolution: "@panva/hkdf@npm:1.1.1"
|
||||||
checksum: f0dd12903751d8792420353f809ed3c7de860cf506399759fff5f59f7acfef8a77e2b64012898cee7e5b047708fa0bd91dff5ef55a502bf8ea11aad9842160da
|
checksum: f0dd12903751d8792420353f809ed3c7de860cf506399759fff5f59f7acfef8a77e2b64012898cee7e5b047708fa0bd91dff5ef55a502bf8ea11aad9842160da
|
||||||
@@ -3286,6 +3347,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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:*":
|
"@types/mime@npm:*":
|
||||||
version: 3.0.4
|
version: 3.0.4
|
||||||
resolution: "@types/mime@npm:3.0.4"
|
resolution: "@types/mime@npm:3.0.4"
|
||||||
@@ -3919,6 +3989,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"accepts@npm:^1.3.7":
|
||||||
version: 1.3.8
|
version: 1.3.8
|
||||||
resolution: "accepts@npm:1.3.8"
|
resolution: "accepts@npm:1.3.8"
|
||||||
@@ -4207,6 +4284,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"assertion-error@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "assertion-error@npm:1.1.0"
|
resolution: "assertion-error@npm:1.1.0"
|
||||||
@@ -4309,6 +4393,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"balanced-match@npm:^1.0.0":
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
resolution: "balanced-match@npm:1.0.2"
|
resolution: "balanced-match@npm:1.0.2"
|
||||||
@@ -4904,13 +4997,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"cookie@npm:^0.4.0":
|
||||||
version: 0.4.2
|
version: 0.4.2
|
||||||
resolution: "cookie@npm:0.4.2"
|
resolution: "cookie@npm:0.4.2"
|
||||||
@@ -4918,6 +5004,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"cookie@npm:~0.6.0":
|
||||||
version: 0.6.0
|
version: 0.6.0
|
||||||
resolution: "cookie@npm:0.6.0"
|
resolution: "cookie@npm:0.6.0"
|
||||||
@@ -4978,6 +5071,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"core-util-is@npm:~1.0.0":
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
resolution: "core-util-is@npm:1.0.3"
|
resolution: "core-util-is@npm:1.0.3"
|
||||||
@@ -6481,6 +6581,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3":
|
||||||
version: 3.1.3
|
version: 3.1.3
|
||||||
resolution: "fast-deep-equal@npm:3.1.3"
|
resolution: "fast-deep-equal@npm:3.1.3"
|
||||||
@@ -7276,7 +7383,6 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "homarr@workspace:."
|
resolution: "homarr@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
"@auth/drizzle-adapter": ^0.3.2
|
|
||||||
"@ctrl/deluge": ^4.1.0
|
"@ctrl/deluge": ^4.1.0
|
||||||
"@ctrl/qbittorrent": ^6.0.0
|
"@ctrl/qbittorrent": ^6.0.0
|
||||||
"@ctrl/shared-torrent": ^4.1.1
|
"@ctrl/shared-torrent": ^4.1.1
|
||||||
@@ -7327,6 +7433,7 @@ __metadata:
|
|||||||
"@types/better-sqlite3": ^7.6.5
|
"@types/better-sqlite3": ^7.6.5
|
||||||
"@types/cookies": ^0.7.7
|
"@types/cookies": ^0.7.7
|
||||||
"@types/dockerode": ^3.3.9
|
"@types/dockerode": ^3.3.9
|
||||||
|
"@types/ldapjs": ^3.0.2
|
||||||
"@types/node": 18.17.8
|
"@types/node": 18.17.8
|
||||||
"@types/prismjs": ^1.26.0
|
"@types/prismjs": ^1.26.0
|
||||||
"@types/react": ^18.2.11
|
"@types/react": ^18.2.11
|
||||||
@@ -7370,9 +7477,8 @@ __metadata:
|
|||||||
i18next: ^22.5.1
|
i18next: ^22.5.1
|
||||||
immer: ^10.0.2
|
immer: ^10.0.2
|
||||||
js-file-download: ^0.4.12
|
js-file-download: ^0.4.12
|
||||||
|
ldapjs: ^3.0.5
|
||||||
mantine-react-table: ^1.3.4
|
mantine-react-table: ^1.3.4
|
||||||
moment: ^2.29.4
|
|
||||||
moment-timezone: ^0.5.43
|
|
||||||
next: 13.4.12
|
next: 13.4.12
|
||||||
next-auth: ^4.23.0
|
next-auth: ^4.23.0
|
||||||
next-i18next: ^14.0.0
|
next-i18next: ^14.0.0
|
||||||
@@ -8188,13 +8294,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"js-file-download@npm:^0.4.12":
|
||||||
version: 0.4.12
|
version: 0.4.12
|
||||||
resolution: "js-file-download@npm:0.4.12"
|
resolution: "js-file-download@npm:0.4.12"
|
||||||
@@ -8398,6 +8497,28 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"levn@npm:^0.4.1":
|
||||||
version: 0.4.1
|
version: 0.4.1
|
||||||
resolution: "levn@npm:0.4.1"
|
resolution: "levn@npm:0.4.1"
|
||||||
@@ -8960,22 +9081,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"mpd-parser@npm:^1.0.1, mpd-parser@npm:^1.2.2":
|
||||||
version: 1.2.2
|
version: 1.2.2
|
||||||
resolution: "mpd-parser@npm:1.2.2"
|
resolution: "mpd-parser@npm:1.2.2"
|
||||||
@@ -9341,13 +9446,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"oauth@npm:^0.9.15":
|
||||||
version: 0.9.15
|
version: 0.9.15
|
||||||
resolution: "oauth@npm:0.9.15"
|
resolution: "oauth@npm:0.9.15"
|
||||||
@@ -9801,17 +9899,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"preact-render-to-string@npm:^5.1.19":
|
||||||
version: 5.2.6
|
version: 5.2.6
|
||||||
resolution: "preact-render-to-string@npm:5.2.6"
|
resolution: "preact-render-to-string@npm:5.2.6"
|
||||||
@@ -9823,13 +9910,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"preact@npm:^10.6.3":
|
||||||
version: 10.19.2
|
version: 10.19.2
|
||||||
resolution: "preact@npm:10.19.2"
|
resolution: "preact@npm:10.19.2"
|
||||||
@@ -9859,6 +9939,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"prelude-ls@npm:^1.2.1":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "prelude-ls@npm:1.2.1"
|
resolution: "prelude-ls@npm:1.2.1"
|
||||||
@@ -9941,6 +10028,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"process@npm:^0.11.10":
|
||||||
version: 0.11.10
|
version: 0.11.10
|
||||||
resolution: "process@npm:0.11.10"
|
resolution: "process@npm:0.11.10"
|
||||||
@@ -12520,6 +12614,37 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"video.js@npm:^7 || ^8, video.js@npm:^8.0.3":
|
||||||
version: 8.6.1
|
version: 8.6.1
|
||||||
resolution: "video.js@npm:8.6.1"
|
resolution: "video.js@npm:8.6.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user