Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46a57c1cf2 | ||
|
|
78527cb375 | ||
|
|
508f687491 | ||
|
|
9a8ea9e1fe | ||
|
|
b1ae5f700e | ||
|
|
f0a67d9a29 | ||
|
|
5d113ea280 | ||
|
|
d45ae5fab9 | ||
|
|
02249d20c2 | ||
|
|
cefa0d8fde | ||
|
|
4933b4f60d | ||
|
|
a60053e6c4 | ||
|
|
6756838d5f | ||
|
|
74fe7a7a28 |
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
|
||||
- 🐳 Easy deployment with Docker, unRAID, and Synology
|
||||
- 🚀 Compatible with any major consumer hardware (x86, Raspberry Pi, old laptops, ...)
|
||||
- 💵 Free and Open-Source - your data stays on your device. No telemetry data.
|
||||
|
||||
<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.
|
||||
Each widget and integration has a comprehensive documentation for your comfort.
|
||||
Homarr will integrate with the following applications of yours:
|
||||
Homarr has a [built-in collection of widgets and integrations](https://homarr.dev/docs/management/integrations/), that connect to your applications and enable you to control them directly from the dashboard.
|
||||
Each widget and integration has a comprehensive documentation
|
||||
Homarr will integrate with the following applications:
|
||||
|
||||
- 📥 Torrent clients
|
||||
- [Deluge](https://homarr.dev/docs/integrations/#deluge)
|
||||
- [Transmission](https://homarr.dev/docs/integrations/#transmission)
|
||||
- [qBittorent](https://homarr.dev/docs/integrations/#qbittorrent-integration)
|
||||
- 📥 Usenet clients
|
||||
- [SABnzbd](https://homarr.dev/docs/integrations/#sabnzbd)
|
||||
- [NZBGet](https://homarr.dev/docs/integrations/#nzbget)
|
||||
- 📚 Media collection managers
|
||||
- [Sonarr](https://homarr.dev/docs/integrations/#sonarr)
|
||||
- [Radarr](https://homarr.dev/docs/integrations/#radarr)
|
||||
- [Lidarr](https://homarr.dev/docs/integrations/#lidarr)
|
||||
- [Readarr](https://homarr.dev/docs/integrations/#readarr)
|
||||
- 🎞️ Media request managers
|
||||
- [Overseerr](https://homarr.dev/docs/integrations/#overseerr--jellyseerr)
|
||||
- [Jellyseerr](https://homarr.dev/docs/integrations/#overseerr--jellyseerr)
|
||||
- 🔌 [Dash.](https://homarr.dev/docs/integrations/#dash)
|
||||
- 🐳 [Docker](https://homarr.dev/docs/integrations/#docker)
|
||||
📥 Torrent clients
|
||||
- [Deluge](https://homarr.dev/docs/management/integrations/torrent-deluge)
|
||||
- [Transmission](https://homarr.dev/docs/management/integrations/torrent-transmission)
|
||||
- [qBittorent](https://homarr.dev/docs/management/integrations/torrent-qbittorrent)
|
||||
|
||||
📥 Usenet clients
|
||||
- [SABnzbd](https://homarr.dev/docs/management/integrations/usenet-sabnzbd)
|
||||
- [NZBGet](https://homarr.dev/docs/management/integrations/usenet-nzbget)
|
||||
|
||||
📺 Media servers
|
||||
- [Plex](https://homarr.dev/docs/management/integrations/media-server-plex)
|
||||
- [Jellyfin](https://homarr.dev/docs/management/integrations/media-server-jellyfin)
|
||||
|
||||
📚 Media collection managers
|
||||
- [Sonarr](https://homarr.dev/docs/management/integrations/servarr-sonarr)
|
||||
- [Radarr](https://homarr.dev/docs/management/integrations/servarr-radarr)
|
||||
- [Lidarr](https://homarr.dev/docs/management/integrations/servarr-lidarr)
|
||||
- [Readarr](https://homarr.dev/docs/management/integrations/servarr-readarr)
|
||||
|
||||
🎞️ Media request managers
|
||||
- [Overseerr](https://homarr.dev/docs/management/integrations/media-requester/)
|
||||
- [Jellyseerr](https://homarr.dev/docs/management/integrations/media-requester/)
|
||||
|
||||
🚫 DNS ad-blockers
|
||||
- [Pihole](https://homarr.dev/docs/management/integrations/dns-pihole)
|
||||
- [AdGuard Home](https://homarr.dev/docs/management/integrations/dns-adguard-home)
|
||||
|
||||
Other integrations
|
||||
- [🔌 Dash.](https://homarr.dev/docs/management/integrations/hardware-dash)
|
||||
- [🐳 Docker](https://homarr.dev/docs/management/integrations/containers-docker)
|
||||
|
||||
We're constantly adding new integrations and widgets, which will enhance your experience even further.
|
||||
|
||||
@@ -123,3 +135,4 @@ You can also support us by helping with [translating the entire project](https:/
|
||||
All contributions, regardless of their size or scope, are welcome and highly appreciated! Thank you ❤️
|
||||
|
||||

|
||||
[](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss)
|
||||
|
||||
@@ -6,6 +6,11 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
});
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
webpack: (config) => {
|
||||
// for dynamic loading of auth providers
|
||||
config.experiments = { ...config.experiments, topLevelAwait: true };
|
||||
return config;
|
||||
},
|
||||
images: {
|
||||
domains: ['cdn.jsdelivr.net'],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "0.14.5",
|
||||
"version": "0.15.0",
|
||||
"description": "Homarr - A homepage for your server.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -27,7 +27,6 @@
|
||||
"db:migrate": "dotenv ts-node drizzle/migrate/migrate.ts ./drizzle"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/drizzle-adapter": "^0.3.2",
|
||||
"@ctrl/deluge": "^4.1.0",
|
||||
"@ctrl/qbittorrent": "^6.0.0",
|
||||
"@ctrl/shared-torrent": "^4.1.1",
|
||||
@@ -92,9 +91,8 @@
|
||||
"i18next": "^22.5.1",
|
||||
"immer": "^10.0.2",
|
||||
"js-file-download": "^0.4.12",
|
||||
"ldapjs": "^3.0.5",
|
||||
"mantine-react-table": "^1.3.4",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"next": "13.4.12",
|
||||
"next-auth": "^4.23.0",
|
||||
"next-i18next": "^14.0.0",
|
||||
@@ -123,6 +121,7 @@
|
||||
"@types/better-sqlite3": "^7.6.5",
|
||||
"@types/cookies": "^0.7.7",
|
||||
"@types/dockerode": "^3.3.9",
|
||||
"@types/ldapjs": "^3.0.2",
|
||||
"@types/node": "18.17.8",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react": "^18.2.11",
|
||||
|
||||
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",
|
||||
"info": "Unique entity ID in Home Assistant. Copy by clicking on entity > Click on cog icon > Click on copy button at 'Entity ID'. Some custom entities may not be supported."
|
||||
},
|
||||
"appendUnit": {
|
||||
"label": "Append unit of measurement",
|
||||
"info": "Append the unit of measurement attribute to the entity state."
|
||||
},
|
||||
"automationId": {
|
||||
"label": "Optional automation ID",
|
||||
"info": "Your unique automation ID. Always starts with automation.XXXXX. If not set, widget will not be clickable and only display state. After click, entity state will be refreshed."
|
||||
},
|
||||
"displayName": {
|
||||
"label": "Display name"
|
||||
},
|
||||
"displayFriendlyName": {
|
||||
"label": "Display friendly name",
|
||||
"info": "Display friendly name from Home Assistant instead instead of display name"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
"label": "Ordina per data di pubblicazione (ascendente)"
|
||||
},
|
||||
"sortPostsWithoutPublishDateToTheTop": {
|
||||
"label": ""
|
||||
"label": "Metti i post senza data di pubblicazione in alto"
|
||||
},
|
||||
"maximumAmountOfPosts": {
|
||||
"label": ""
|
||||
"label": "Numero massimo di post"
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"previous": "Iepriekšējais",
|
||||
"confirm": "Apstipriniet",
|
||||
"enabled": "Iespējots",
|
||||
"duplicate": "",
|
||||
"duplicate": "Dublicēt",
|
||||
"disabled": "Atspējots",
|
||||
"enableAll": "Iespējot visu",
|
||||
"disableAll": "Atspējot visu",
|
||||
@@ -54,5 +54,5 @@
|
||||
"height": "Augstums"
|
||||
},
|
||||
"public": "Publisks",
|
||||
"restricted": ""
|
||||
"restricted": "Ierobežots"
|
||||
}
|
||||
@@ -22,5 +22,5 @@
|
||||
"message": "Ir izveidota kategorija \"{{name}}\""
|
||||
}
|
||||
},
|
||||
"importFromDocker": ""
|
||||
"importFromDocker": "Importēt no Docker"
|
||||
}
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
"label": "Neatgriezeniski dzēst",
|
||||
"disabled": "Dzēšana atspējota, jo vecāki Homarr komponenti neļauj dzēst noklusējuma konfigurāciju. Dzēšana būs iespējama nākotnē."
|
||||
},
|
||||
"duplicate": "",
|
||||
"duplicate": "Dublicēt",
|
||||
"rename": {
|
||||
"label": "",
|
||||
"label": "Pārdēvēt",
|
||||
"modal": {
|
||||
"title": "",
|
||||
"title": "Pārdēvēt dēli {{name}}",
|
||||
"fields": {
|
||||
"name": {
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
"label": "Jauns nosaukums",
|
||||
"placeholder": "Jauns dēļa nosaukums"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
},
|
||||
"filter": {
|
||||
"roles": {
|
||||
"all": "",
|
||||
"normal": "",
|
||||
"admin": "",
|
||||
"owner": ""
|
||||
"all": "Viss",
|
||||
"normal": "Normāls",
|
||||
"admin": "Administrators",
|
||||
"owner": "Īpašnieks"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"metaTitle": "",
|
||||
"back": "",
|
||||
"metaTitle": "Lietotājs {{username}}",
|
||||
"back": "Atgriezties uz lietotāju pārvaldību",
|
||||
"sections": {
|
||||
"general": {
|
||||
"title": "Vispārīgi",
|
||||
@@ -14,40 +14,40 @@
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"title": "",
|
||||
"title": "Drošība",
|
||||
"inputs": {
|
||||
"password": {
|
||||
"label": ""
|
||||
"label": "Jauna parole"
|
||||
},
|
||||
"terminateExistingSessions": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Pārtraukt esošās sesijas",
|
||||
"description": "Piespiež lietotāju no jauna pieteikties savās ierīcēs"
|
||||
},
|
||||
"confirm": {
|
||||
"label": "Apstipriniet",
|
||||
"description": ""
|
||||
"description": "Parole tiks atjaunināta. Šo darbību nevar atcelt."
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"title": "",
|
||||
"currentRole": "",
|
||||
"title": "Lomas",
|
||||
"currentRole": "Pašreizējā loma: ",
|
||||
"badges": {
|
||||
"owner": "",
|
||||
"admin": "",
|
||||
"normal": ""
|
||||
"owner": "Īpašnieks",
|
||||
"admin": "Administrators",
|
||||
"normal": "Normāls"
|
||||
}
|
||||
},
|
||||
"deletion": {
|
||||
"title": "",
|
||||
"title": "Konta dzēšana",
|
||||
"inputs": {
|
||||
"confirmUsername": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Apstiprināt lietotājvārdu",
|
||||
"description": "Ierakstiet lietotājvārdu, lai apstiprinātu dzēšanu"
|
||||
},
|
||||
"confirm": {
|
||||
"label": "Neatgriezeniski dzēst",
|
||||
"description": ""
|
||||
"description": "Es apzinos, ka šī darbība ir neatgriezeniska un visi konta dati tiks zaudēti."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"settings": {
|
||||
"title": "Datuma un Laika logrīka iestatījumi",
|
||||
"timezone": {
|
||||
"label": "",
|
||||
"info": ""
|
||||
"label": "Laika zona",
|
||||
"info": "Izvēlieties savas laika zonas nosaukumu, atrodiet savējo šeit: "
|
||||
},
|
||||
"customTitle": {
|
||||
"label": ""
|
||||
"label": "Pilsētas nosaukums vai pielāgots nosaukums"
|
||||
},
|
||||
"display24HourFormat": {
|
||||
"label": "Rādīt pilnu laiku (24 stundu)"
|
||||
@@ -21,11 +21,11 @@
|
||||
}
|
||||
},
|
||||
"titleState": {
|
||||
"label": "",
|
||||
"info": "",
|
||||
"label": "Pulksteņa nosaukums",
|
||||
"info": "Pielāgotais nosaukums un laika zonas kods var tikt parādīts jūsu logrīkā.<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": {
|
||||
"both": "",
|
||||
"city": "",
|
||||
"both": "Pilsēta un Laika zona",
|
||||
"city": "Tikai nosaukums",
|
||||
"none": "Nekas"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
"label": "Teksta līniju skava"
|
||||
},
|
||||
"sortByPublishDateAscending": {
|
||||
"label": ""
|
||||
"label": "Kārtot pēc publicēšanas datuma (augošā secībā)"
|
||||
},
|
||||
"sortPostsWithoutPublishDateToTheTop": {
|
||||
"label": ""
|
||||
"label": "Ievietot ziņas bez publicēšanas datuma augšpusē"
|
||||
},
|
||||
"maximumAmountOfPosts": {
|
||||
"label": ""
|
||||
"label": "Maksimālais ierakstu skaits"
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"entityNotFound": "",
|
||||
"entityNotFound": "Vienība nav atrasta",
|
||||
"descriptor": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Home Assistant vienība",
|
||||
"description": "Vienības pašreizējais stāvoklis pakalpojumā Home Assistant",
|
||||
"settings": {
|
||||
"title": "",
|
||||
"title": "Vienības stāvoklis",
|
||||
"entityId": {
|
||||
"label": "",
|
||||
"info": ""
|
||||
"label": "Vienības ID",
|
||||
"info": "Unikāls vienības ID pakalpojumā Home Assistant. Ievietojiet starpliktuvē, noklikšķinot uz vienību > Noklikšķiniet uz zobrata ikonu > Noklikšķiniet uz kopēšanas pogu pie \"Vienības ID\". Dažas pielāgotas vienības var nebūt atbalstītas."
|
||||
},
|
||||
"automationId": {
|
||||
"label": "",
|
||||
"info": ""
|
||||
"label": "Izvēles automatizācijas ID",
|
||||
"info": "Jūsu unikālais automatizācijas ID. Vienmēr sākas ar automatizāciju.XXXX. Ja tas nav iestatīts, logrīks nebūs noklikšķināms un tiks parādīts tikai statuss. Pēc noklikšķināšanas vienības stāvoklis tiks atsvaidzināts."
|
||||
},
|
||||
"displayName": {
|
||||
"label": ""
|
||||
"label": "Parādāmais nosaukums"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Home Assistant automatizācija",
|
||||
"description": "Automatizācijas izpilde",
|
||||
"settings": {
|
||||
"title": "",
|
||||
"title": "Automatizācijas izpilde",
|
||||
"automationId": {
|
||||
"label": "",
|
||||
"info": ""
|
||||
"label": "Automatizācijas ID",
|
||||
"info": "Jūsu unikālais automatizācijas ID. Vienmēr sāksies ar automation.XXXXX."
|
||||
},
|
||||
"displayName": {
|
||||
"label": ""
|
||||
"label": "Parādāmais nosaukums"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,22 +41,22 @@
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"isCompleted": "",
|
||||
"isCompleted": "Lejupielādē",
|
||||
"name": "Nosaukums",
|
||||
"dateAdded": "",
|
||||
"dateAdded": "Pievienots",
|
||||
"size": "Lielums",
|
||||
"download": "Lejupielāde",
|
||||
"upload": "Augšupielāde",
|
||||
"estimatedTimeOfArrival": "ETA",
|
||||
"progress": "Progress",
|
||||
"totalUploaded": "",
|
||||
"totalDownloaded": "",
|
||||
"ratio": "",
|
||||
"seeds": "",
|
||||
"peers": "",
|
||||
"label": "",
|
||||
"totalUploaded": "Kopējā Augšupielāde",
|
||||
"totalDownloaded": "Kopējā Lejupielāde",
|
||||
"ratio": "Attiecība",
|
||||
"seeds": "Devēji (savienoti)",
|
||||
"peers": "Ņēmēji (savienoti)",
|
||||
"label": "Birka",
|
||||
"state": "Stāvoklis",
|
||||
"stateMessage": ""
|
||||
"stateMessage": "Statusa Ziņojums"
|
||||
},
|
||||
"item": {
|
||||
"text": "Pārvalda {{appName}}, {{ratio}} attiecība"
|
||||
|
||||
@@ -19,26 +19,26 @@
|
||||
"label": "Fons"
|
||||
},
|
||||
"backgroundImageAttachment": {
|
||||
"label": "",
|
||||
"label": "Fona attēla pielikums",
|
||||
"options": {
|
||||
"fixed": "",
|
||||
"scroll": ""
|
||||
"fixed": "Fiksēts - fons paliek nemainīgā pozīcijā (ieteicams)",
|
||||
"scroll": "Ritināšana - fons ritinās līdz ar kursora ritināšanu"
|
||||
}
|
||||
},
|
||||
"backgroundImageSize": {
|
||||
"label": "",
|
||||
"label": "Fona attēla izmērs",
|
||||
"options": {
|
||||
"cover": "",
|
||||
"contain": ""
|
||||
"cover": "Pārklājums - pēc iespējas mazāks attēla mērogs, lai, apgriežot lieko vietu, pārklātu visu logu. (ieteicams)",
|
||||
"contain": "Saturēt — mērogo attēlu pēc iespējas lielāku tā konteinerā, neapgriežot vai neizstiepjot attēlu."
|
||||
}
|
||||
},
|
||||
"backgroundImageRepeat": {
|
||||
"label": "",
|
||||
"label": "Fona attēla pielikums",
|
||||
"options": {
|
||||
"repeat": "",
|
||||
"no-repeat": "",
|
||||
"repeat-x": "",
|
||||
"repeat-y": ""
|
||||
"repeat": "Atkārtot — attēls tiek atkārtots tik daudz, cik nepieciešams, lai aptvertu visu fona laukumu.",
|
||||
"no-repeat": "Bez atkārtojuma - attēls neatkārtojas un var neaizpildīt visu fona laukumu (ieteicams)",
|
||||
"repeat-x": "Atkārtot X - tāpat kā \"Atkārtot\", bet tikai uz horizontālās ass.",
|
||||
"repeat-y": "Atkārtot Y - tāpat kā \"Atkārtot\", bet tikai uz vertikālās ass."
|
||||
}
|
||||
},
|
||||
"customCSS": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"title": "Docker",
|
||||
"alerts": {
|
||||
"notConfigured": {
|
||||
"text": ""
|
||||
"text": "Jūsu Homarr instancē nav konfigurēts Docker vai arī nav izdevies iegūtu konteinerus. Lūdzu, pārbaudiet dokumentāciju par to, kā iestatīt integrāciju."
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"previous": "Tidligere",
|
||||
"confirm": "Bekreft",
|
||||
"enabled": "Aktivert",
|
||||
"duplicate": "",
|
||||
"duplicate": "Dupliser",
|
||||
"disabled": "Deaktivert",
|
||||
"enableAll": "Aktiver alle",
|
||||
"disableAll": "Deaktiver alle",
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"message": "Kategorien \"{{name}}\" er opprettet"
|
||||
}
|
||||
},
|
||||
"importFromDocker": ""
|
||||
"importFromDocker": "Importer fra docker"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"switchTheme": "Bytt tema",
|
||||
"preferences": "Brukerinnstillinger",
|
||||
"defaultBoard": "Standard dashbord",
|
||||
"manage": "Endre",
|
||||
"manage": "Administrer",
|
||||
"logout": "Logg ut fra {{username}}",
|
||||
"login": "Logg Inn"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"users": {
|
||||
"title": "Brukere",
|
||||
"items": {
|
||||
"manage": "Endre",
|
||||
"manage": "Administrer",
|
||||
"invites": "Invitasjoner"
|
||||
}
|
||||
},
|
||||
@@ -26,7 +26,7 @@
|
||||
"title": "Verktøy",
|
||||
"items": {
|
||||
"docker": "Docker",
|
||||
"api": ""
|
||||
"api": "API"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"description": "Homarr er et <strong>sleek</strong>, <strong>moderne</strong> dashbord som legger alle apper og tjenester ved fingertuppene dine. Med Homarr, kan du få tilgang til og kontrollere alt på et bekvemt sted. Homarr sømløst integrerer med appene du har lagt til, gir deg verdifull informasjon og gir deg full kontroll. Installasjon er en lek, og Homarr støtter en lang rekke distribusjonsmetoder.",
|
||||
"addToDashboard": "Legg til på dashbord",
|
||||
"addToDashboard": "Legg til tavlen",
|
||||
"tip": "Mod refererer til endringstasten din, den er Ctrl og Command/Super/Windows tasten",
|
||||
"key": "Hurtigtast",
|
||||
"action": "Handling",
|
||||
@@ -20,7 +20,7 @@
|
||||
"version": "Versjon",
|
||||
"nodeEnvironment": "Node miljø",
|
||||
"i18n": "Lastet I18n oversettelsesnavneområder",
|
||||
"locales": "Konfigurerte I18n-lokasjoner",
|
||||
"locales": "Konfigurerte I18n-lokaliseringer",
|
||||
"experimental_disableEditMode": "<b>EXPERIMENTAL</b>: Deaktivere redigeringsmodus"
|
||||
},
|
||||
"version": {
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
"label": "Slett permanent",
|
||||
"disabled": "Sletting er deaktivert fordi eldre Homarr-komponenter ikke tillater sletting av standardkonfigurasjonen. Sletting vil være mulig i fremtiden."
|
||||
},
|
||||
"duplicate": "",
|
||||
"duplicate": "Dupliser",
|
||||
"rename": {
|
||||
"label": "",
|
||||
"label": "Gi nytt navn",
|
||||
"modal": {
|
||||
"title": "",
|
||||
"title": "Gi nytt navn til tavlen {{name}}",
|
||||
"fields": {
|
||||
"name": {
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
"label": "Nytt navn",
|
||||
"placeholder": "Nytt tavlenavn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
},
|
||||
"filter": {
|
||||
"roles": {
|
||||
"all": "",
|
||||
"normal": "",
|
||||
"admin": "",
|
||||
"owner": ""
|
||||
"all": "Alle",
|
||||
"normal": "Normal",
|
||||
"admin": "Administrator",
|
||||
"owner": "Eier"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"metaTitle": "",
|
||||
"back": "",
|
||||
"metaTitle": "Bruker {{username}}",
|
||||
"back": "Tilbake til brukeradministrasjon",
|
||||
"sections": {
|
||||
"general": {
|
||||
"title": "Generelt",
|
||||
@@ -14,40 +14,40 @@
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"title": "",
|
||||
"title": "Sikkerhet",
|
||||
"inputs": {
|
||||
"password": {
|
||||
"label": ""
|
||||
"label": "Nytt passord"
|
||||
},
|
||||
"terminateExistingSessions": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Avslutt eksisterende økter",
|
||||
"description": "Tvinger brukeren til å logge på igjen på enhetene sine"
|
||||
},
|
||||
"confirm": {
|
||||
"label": "Bekreft",
|
||||
"description": ""
|
||||
"description": "Passordet vil bli oppdatert. Handlingen kan ikke tilbakestilles."
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"title": "",
|
||||
"currentRole": "",
|
||||
"title": "Roller",
|
||||
"currentRole": "Nåværende rolle: ",
|
||||
"badges": {
|
||||
"owner": "",
|
||||
"admin": "",
|
||||
"normal": ""
|
||||
"owner": "Eier",
|
||||
"admin": "Administrator",
|
||||
"normal": "Normal"
|
||||
}
|
||||
},
|
||||
"deletion": {
|
||||
"title": "",
|
||||
"title": "Konto sletting",
|
||||
"inputs": {
|
||||
"confirmUsername": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Bekreft brukernavn",
|
||||
"description": "Skriv inn brukernavn for å bekrefte sletting"
|
||||
},
|
||||
"confirm": {
|
||||
"label": "Slett permanent",
|
||||
"description": ""
|
||||
"description": "Jeg er klar over at denne handlingen er permanent og alle kontodata vil gå tapt."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"settings": {
|
||||
"title": "Innstillinger for dato og klokkeslett widget",
|
||||
"timezone": {
|
||||
"label": "",
|
||||
"info": ""
|
||||
"label": "Tidssone",
|
||||
"info": "Velg navnet på tidssonen din, finn din her: "
|
||||
},
|
||||
"customTitle": {
|
||||
"label": ""
|
||||
"label": "Bynavn eller egendefinert tittel"
|
||||
},
|
||||
"display24HourFormat": {
|
||||
"label": "Vis 24 timers formatering"
|
||||
@@ -21,11 +21,11 @@
|
||||
}
|
||||
},
|
||||
"titleState": {
|
||||
"label": "",
|
||||
"info": "",
|
||||
"label": "Klokketittel",
|
||||
"info": "Den egendefinerte tittelen og tidssonekoden kan vises på widgeten din.<br/>Du kan også vise byen alene, vise ingen,<br/>eller til og med vise tidssonen alene når begge er valgt, men ingen tittel er gitt.",
|
||||
"data": {
|
||||
"both": "",
|
||||
"city": "",
|
||||
"both": "Tittel og tidssone",
|
||||
"city": "Kun tittel",
|
||||
"none": "Ingen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "Nedlastingshastighet",
|
||||
"name": "Nedlastings- hastighet",
|
||||
"description": "Viser nedlastingshastighet og opplastingshastighet av støttede integrasjoner."
|
||||
},
|
||||
"card": {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"approved": "Godkjent",
|
||||
"pendingApproval": "Venter på godkjenning",
|
||||
"declined": "Avvist",
|
||||
"available": "",
|
||||
"partial": ""
|
||||
"available": "Tilgjengelig",
|
||||
"partial": "Delvis"
|
||||
},
|
||||
"tooltips": {
|
||||
"approve": "Godkjenne forespørsler",
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
"label": "Tekstlinjer klemme"
|
||||
},
|
||||
"sortByPublishDateAscending": {
|
||||
"label": ""
|
||||
"label": "Sorter etter publiseringsdato (stigende)"
|
||||
},
|
||||
"sortPostsWithoutPublishDateToTheTop": {
|
||||
"label": ""
|
||||
"label": "Sett innlegg uten publiseringsdato øverst"
|
||||
},
|
||||
"maximumAmountOfPosts": {
|
||||
"label": ""
|
||||
"label": "Maksimalt antall innlegg"
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"info": "Unik enhets-ID i Home Assistant. Kopier ved å klikke på enhet > Klikk på tannhjulikonet > Klikk på kopieringsknappen ved Entitets-ID. Noen egendefinerte enheter støttes kanskje ikke."
|
||||
},
|
||||
"automationId": {
|
||||
"label": "",
|
||||
"info": ""
|
||||
"label": "Valgfri automatisering ID",
|
||||
"info": "Din unike automatisering ID. Starter alltid med automation.XXXXX. Hvis den ikke er angitt, vil widgeten ikke være klikkbar og kun vise status Etter klikk vil enhetsstatus bli oppdatert."
|
||||
},
|
||||
"displayName": {
|
||||
"label": "Visningsnavn"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"descriptor": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Home Assistant automatisering",
|
||||
"description": "Utfør en automatisering",
|
||||
"settings": {
|
||||
"title": "",
|
||||
"title": "Utfør en automatisering",
|
||||
"automationId": {
|
||||
"label": "",
|
||||
"info": ""
|
||||
"label": "Automatisering ID",
|
||||
"info": "Din unike automatisering ID. Starter alltid med automation.XXXXX."
|
||||
},
|
||||
"displayName": {
|
||||
"label": "Visningsnavn"
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
"label": "Zoradiť podľa dátumu vydania (vzostupne)"
|
||||
},
|
||||
"sortPostsWithoutPublishDateToTheTop": {
|
||||
"label": ""
|
||||
"label": "Umiestnite príspevky bez dátumu uverejnenia na začiatok"
|
||||
},
|
||||
"maximumAmountOfPosts": {
|
||||
"label": ""
|
||||
"label": "Maximálny počet príspevkov"
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"disabled": "Pasif",
|
||||
"enableAll": "Tümünü etkinleştir",
|
||||
"disableAll": "Tümünü pasifleştir",
|
||||
"version": "Versiyon",
|
||||
"changePosition": "Pozisyon değiştir",
|
||||
"version": "Sürüm",
|
||||
"changePosition": "Pozisyonu değiştir",
|
||||
"remove": "Kaldır",
|
||||
"removeConfirm": "{{item}}'i kaldırmak istediğinizden emin misiniz?",
|
||||
"createItem": "+ yeni {{item}}",
|
||||
|
||||
@@ -163,6 +163,11 @@ export const availableIntegrations = [
|
||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png',
|
||||
label: 'Readarr',
|
||||
},
|
||||
{
|
||||
value: 'prowlarr',
|
||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/prowlarr.png',
|
||||
label: 'Prowlarr',
|
||||
},
|
||||
{
|
||||
value: 'jellyfin',
|
||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png',
|
||||
@@ -186,6 +191,6 @@ export const availableIntegrations = [
|
||||
{
|
||||
value: 'homeAssistant',
|
||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png',
|
||||
label: 'Home Assistant'
|
||||
}
|
||||
label: 'Home Assistant',
|
||||
},
|
||||
] as const satisfies Readonly<SelectItem[]>;
|
||||
|
||||
@@ -9,12 +9,15 @@ import { createBoardSchemaValidation } from '~/validations/boards';
|
||||
|
||||
export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
|
||||
const { t } = useTranslation('manage/boards');
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const { isLoading, mutate } = api.config.save.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.boards.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
onError: async (error) => {
|
||||
form.setFieldError('name', error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
@@ -31,6 +34,7 @@ export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
|
||||
mutate({
|
||||
name: form.values.name,
|
||||
config: fallbackConfig,
|
||||
create: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -59,7 +63,7 @@ export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={async () => {
|
||||
umami.track('Create new board')
|
||||
umami.track('Create new board');
|
||||
}}
|
||||
disabled={isLoading}
|
||||
variant="light"
|
||||
|
||||
@@ -111,7 +111,7 @@ export const ReviewInputStep = ({ values, prevStep, nextStep }: ReviewInputStepP
|
||||
password: values.security.password,
|
||||
email: values.account.eMail === '' ? undefined : values.account.eMail,
|
||||
});
|
||||
umami.track('Create user', { username: values.account.username});
|
||||
umami.track('Create user', { username: values.account.username });
|
||||
}}
|
||||
loading={isLoading}
|
||||
rightIcon={<IconCheck size="1rem" />}
|
||||
|
||||
@@ -57,9 +57,12 @@ export const StepCreateAccount = ({
|
||||
Create your administrator account
|
||||
</Title>
|
||||
<Text>
|
||||
Your administrator account <b>must be secure</b>, that's why we have so many rules surrounding it.
|
||||
<br/>Try not to make it adminadmin this time...
|
||||
<br/>Note: these password requirements <b>are not forced</b>, they are just recommendations.
|
||||
Your administrator account <b>must be secure</b>, that's why we have so many rules
|
||||
surrounding it.
|
||||
<br />
|
||||
Try not to make it adminadmin this time...
|
||||
<br />
|
||||
Note: these password requirements <b>are not forced</b>, they are just recommendations.
|
||||
</Text>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
|
||||
60
src/env.js
60
src/env.js
@@ -1,6 +1,14 @@
|
||||
const { z } = require('zod');
|
||||
const { createEnv } = require('@t3-oss/env-nextjs');
|
||||
|
||||
const trueStrings = ["1", "t", "T", "TRUE", "true", "True"];
|
||||
const falseStrings = ["0", "f", "F", "FALSE", "false", "False"];
|
||||
|
||||
const zodParsedBoolean = () => z
|
||||
.enum([...trueStrings, ...falseStrings])
|
||||
.default("false")
|
||||
.transform((value) => trueStrings.includes(value))
|
||||
|
||||
const portSchema = z
|
||||
.string()
|
||||
.regex(/\d*/)
|
||||
@@ -8,6 +16,8 @@ const portSchema = z
|
||||
.optional();
|
||||
const envSchema = z.enum(['development', 'test', 'production']);
|
||||
|
||||
const authProviders = process.env.AUTH_PROVIDER?.replaceAll(' ', '').split(',') || ['credentials'];
|
||||
|
||||
const env = createEnv({
|
||||
/**
|
||||
* Specify your server-side environment variables schema here. This way you can ensure the app
|
||||
@@ -28,6 +38,37 @@ const env = createEnv({
|
||||
DOCKER_PORT: portSchema,
|
||||
DEMO_MODE: z.string().optional(),
|
||||
HOSTNAME: z.string().optional(),
|
||||
|
||||
// Authentication
|
||||
AUTH_PROVIDER: z.string().default('credentials').transform(providers => providers.replaceAll(' ', '').split(',')),
|
||||
// LDAP
|
||||
...(authProviders.includes('ldap')
|
||||
? {
|
||||
AUTH_LDAP_URI: z.string().url(),
|
||||
AUTH_LDAP_BIND_DN: z.string(),
|
||||
AUTH_LDAP_BIND_PASSWORD: z.string(),
|
||||
AUTH_LDAP_BASE: z.string(),
|
||||
AUTH_LDAP_USERNAME_ATTRIBUTE: z.string().default('uid'),
|
||||
AUTH_LDAP_GROUP_CLASS: z.string().default('groupOfUniqueNames'),
|
||||
AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE: z.string().default('member'),
|
||||
AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE: z.string().default('dn'),
|
||||
AUTH_LDAP_ADMIN_GROUP: z.string().default('admin'),
|
||||
AUTH_LDAP_OWNER_GROUP: z.string().default('admin'),
|
||||
}
|
||||
: {}),
|
||||
// OIDC
|
||||
...(authProviders.includes('oidc')
|
||||
? {
|
||||
AUTH_OIDC_CLIENT_ID: z.string(),
|
||||
AUTH_OIDC_CLIENT_SECRET: z.string(),
|
||||
AUTH_OIDC_URI: z.string().url(),
|
||||
// Custom Display name, defaults to OIDC
|
||||
AUTH_OIDC_CLIENT_NAME: z.string().default('OIDC'),
|
||||
AUTH_OIDC_ADMIN_GROUP: z.string().default('admin'),
|
||||
AUTH_OIDC_OWNER_GROUP: z.string().default('admin'),
|
||||
AUTH_OIDC_AUTO_LOGIN: zodParsedBoolean()
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -64,6 +105,25 @@ const env = createEnv({
|
||||
NEXT_PUBLIC_PORT: process.env.PORT,
|
||||
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
|
||||
HOSTNAME: process.env.HOSTNAME,
|
||||
AUTH_PROVIDER: process.env.AUTH_PROVIDER,
|
||||
AUTH_LDAP_URI: process.env.AUTH_LDAP_URI,
|
||||
AUTH_LDAP_BIND_DN: process.env.AUTH_LDAP_BIND_DN,
|
||||
AUTH_LDAP_BIND_PASSWORD: process.env.AUTH_LDAP_BIND_PASSWORD,
|
||||
AUTH_LDAP_BASE: process.env.AUTH_LDAP_BASE,
|
||||
AUTH_LDAP_USERNAME_ATTRIBUTE: process.env.AUTH_LDAP_USERNAME_ATTRIBUTE,
|
||||
AUTH_LDAP_GROUP_CLASS: process.env.AUTH_LDAP_GROUP_CLASS,
|
||||
AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE: process.env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE,
|
||||
AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE: process.env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE,
|
||||
AUTH_LDAP_ADMIN_GROUP: process.env.AUTH_LDAP_ADMIN_GROUP,
|
||||
AUTH_LDAP_OWNER_GROUP: process.env.AUTH_LDAP_OWNER_GROUP,
|
||||
AUTH_OIDC_CLIENT_ID: process.env.AUTH_OIDC_CLIENT_ID,
|
||||
AUTH_OIDC_CLIENT_SECRET: process.env.AUTH_OIDC_CLIENT_SECRET,
|
||||
AUTH_OIDC_URI: process.env.AUTH_OIDC_URI,
|
||||
AUTH_OIDC_CLIENT_NAME: process.env.AUTH_OIDC_CLIENT_NAME,
|
||||
AUTH_OIDC_GROUP_CLAIM: process.env.AUTH_OIDC_GROUP_CLAIM,
|
||||
AUTH_OIDC_ADMIN_GROUP: process.env.AUTH_OIDC_ADMIN_GROUP,
|
||||
AUTH_OIDC_OWNER_GROUP: process.env.AUTH_OIDC_OWNER_GROUP,
|
||||
AUTH_OIDC_AUTO_LOGIN: process.env.AUTH_OIDC_AUTO_LOGIN,
|
||||
DEMO_MODE: process.env.DEMO_MODE,
|
||||
},
|
||||
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||
|
||||
@@ -10,6 +10,7 @@ const skippedUrls = [
|
||||
'/favicon.ico',
|
||||
'/404',
|
||||
'/pages/_app',
|
||||
'/auth/login',
|
||||
'/imgs/',
|
||||
];
|
||||
|
||||
@@ -29,12 +30,15 @@ export async function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
// Do not redirect if there are users in the database
|
||||
if (cachedUserCount > 0) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
if (cachedUserCount > 0 || !(await shouldRedirectToOnboard())) {
|
||||
// redirect to login if not logged in
|
||||
// not working, should work in next-auth 5
|
||||
// @see https://github.com/nextauthjs/next-auth/pull/7443
|
||||
|
||||
// Do not redirect if there are users in the database
|
||||
if (!(await shouldRedirectToOnboard())) {
|
||||
// const session = await getServerSession();
|
||||
// if (!session?.user) {
|
||||
// return NextResponse.redirect(getUrl(req) + '/auth/login')
|
||||
// }
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
|
||||
@@ -125,8 +125,7 @@ export default function AuthInvitePage() {
|
||||
withAsterisk
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
<Card
|
||||
>
|
||||
<Card>
|
||||
<PasswordRequirements value={form.values.password} />
|
||||
</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 { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { ThemeSchemeToggle } from '~/components/ThemeSchemeToggle/ThemeSchemeToggle';
|
||||
import { FloatingBackground } from '~/components/layout/Background/FloatingBackground';
|
||||
@@ -17,8 +28,13 @@ import { getServerSideTranslations } from '~/tools/server/getServerSideTranslati
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { signInSchema } from '~/validations/user';
|
||||
|
||||
const signInSchemaWithProvider = signInSchema.extend({ provider: z.string() });
|
||||
|
||||
export default function LoginPage({
|
||||
redirectAfterLogin,
|
||||
providers,
|
||||
oidcProviderName,
|
||||
oidcAutoLogin,
|
||||
isDemo,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const { t } = useTranslation('authentication/login');
|
||||
@@ -27,16 +43,18 @@ export default function LoginPage({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof signInSchema>>({
|
||||
const hasCredentialsInput = providers.includes('credentials') || providers.includes('ldap');
|
||||
|
||||
const form = useForm<z.infer<typeof signInSchemaWithProvider>>({
|
||||
validateInputOnChange: 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);
|
||||
setIsError(false);
|
||||
signIn('credentials', {
|
||||
signIn(values.provider, {
|
||||
redirect: false,
|
||||
name: values.name,
|
||||
password: values.password,
|
||||
@@ -51,6 +69,10 @@ export default function LoginPage({
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (oidcAutoLogin) signIn('oidc');
|
||||
}, [oidcAutoLogin]);
|
||||
|
||||
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||
|
||||
return (
|
||||
@@ -58,7 +80,6 @@ export default function LoginPage({
|
||||
<Head>
|
||||
<title>{metaTitle}</title>
|
||||
</Head>
|
||||
|
||||
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
||||
<FloatingBackground />
|
||||
<ThemeSchemeToggle pos="absolute" top={20} right={20} />
|
||||
@@ -83,51 +104,94 @@ export default function LoginPage({
|
||||
<b>demodemo</b>
|
||||
</Alert>
|
||||
)}
|
||||
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={450}>
|
||||
<Title style={{ whiteSpace: 'nowrap' }} align="center" weight={900}>
|
||||
{t('title')}
|
||||
</Title>
|
||||
{oidcAutoLogin ? (
|
||||
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={450}>
|
||||
<Text size="lg" align="center" m="md">
|
||||
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">
|
||||
{t('text')}
|
||||
</Text>
|
||||
<Text color="dimmed" size="sm" align="center" mt={5} mb="md">
|
||||
{t('text')}
|
||||
</Text>
|
||||
|
||||
{isError && (
|
||||
<Alert icon={<IconAlertTriangle size="1rem" />} color="red">
|
||||
{t('alert')}
|
||||
</Alert>
|
||||
)}
|
||||
{isError && (
|
||||
<Alert icon={<IconAlertTriangle size="1rem" />} color="red">
|
||||
{t('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)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label={t('form.fields.username.label')}
|
||||
autoComplete="homarr-username"
|
||||
withAsterisk
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
label={t('form.fields.password.label')}
|
||||
autoComplete="homarr-password"
|
||||
withAsterisk
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
label={t('form.fields.password.label')}
|
||||
autoComplete="homarr-password"
|
||||
withAsterisk
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
{providers.includes('credentials') && (
|
||||
<Button
|
||||
mt="xs"
|
||||
variant="light"
|
||||
fullWidth
|
||||
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}>
|
||||
{t('form.buttons.submit')}
|
||||
{providers.includes('ldap') && (
|
||||
<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>
|
||||
|
||||
{redirectAfterLogin && (
|
||||
<Text color="dimmed" align="center" size="xs">
|
||||
{t('form.afterLoginRedirection', { url: redirectAfterLogin })}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</Stack>
|
||||
</Flex>
|
||||
</>
|
||||
@@ -136,7 +200,12 @@ export default function LoginPage({
|
||||
|
||||
const regexExp = /^\/{1}[A-Za-z\/]*$/;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ locale, req, res, query }) => {
|
||||
export const getServerSideProps = async ({
|
||||
locale,
|
||||
req,
|
||||
res,
|
||||
query,
|
||||
}: GetServerSidePropsContext) => {
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
|
||||
const zodResult = await z
|
||||
@@ -159,6 +228,9 @@ export const getServerSideProps: GetServerSideProps = async ({ locale, req, res,
|
||||
props: {
|
||||
...(await getServerSideTranslations(['authentication/login'], locale, req, res)),
|
||||
redirectAfterLogin,
|
||||
providers: env.AUTH_PROVIDER,
|
||||
oidcProviderName: env.AUTH_OIDC_CLIENT_NAME || null,
|
||||
oidcAutoLogin: env.AUTH_OIDC_AUTO_LOGIN || null,
|
||||
isDemo,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
return result;
|
||||
}
|
||||
|
||||
const isDockerEnabled: boolean = !!env.DOCKER_HOST || !!env.DOCKER_PORT || fs.existsSync('/var/run/docker');
|
||||
const isDockerEnabled: boolean = !!env.DOCKER_HOST || !!env.DOCKER_PORT || fs.existsSync('/var/run/docker.sock');
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -46,7 +46,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
return result;
|
||||
}
|
||||
|
||||
const isDockerEnabled: boolean = !!env.DOCKER_HOST || !!env.DOCKER_PORT || fs.existsSync('/var/run/docker');
|
||||
const isDockerEnabled: boolean = !!env.DOCKER_HOST || !!env.DOCKER_PORT || fs.existsSync('/var/run/docker.sock');
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createTRPCRouter } from '~/server/api/trpc';
|
||||
|
||||
import { appRouter } from './routers/app';
|
||||
import { boardRouter } from './routers/board';
|
||||
import { calendarRouter } from './routers/calendar';
|
||||
@@ -8,6 +9,7 @@ import { dnsHoleRouter } from './routers/dns-hole/router';
|
||||
import { dockerRouter } from './routers/docker/router';
|
||||
import { downloadRouter } from './routers/download';
|
||||
import { iconRouter } from './routers/icon';
|
||||
import { indexerManagerRouter } from './routers/indexer-manager';
|
||||
import { inviteRouter } from './routers/invite/invite-router';
|
||||
import { mediaRequestsRouter } from './routers/media-request';
|
||||
import { mediaServerRouter } from './routers/media-server';
|
||||
@@ -30,6 +32,7 @@ export const rootRouter = createTRPCRouter({
|
||||
rss: rssRouter,
|
||||
user: userRouter,
|
||||
calendar: calendarRouter,
|
||||
indexerManager: indexerManagerRouter,
|
||||
config: configRouter,
|
||||
dashDot: dashDotRouter,
|
||||
dnsHole: dnsHoleRouter,
|
||||
@@ -45,7 +48,7 @@ export const rootRouter = createTRPCRouter({
|
||||
boards: boardRouter,
|
||||
password: passwordRouter,
|
||||
notebook: notebookRouter,
|
||||
smartHomeEntityState: smartHomeEntityStateRouter
|
||||
smartHomeEntityState: smartHomeEntityStateRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -19,15 +19,15 @@ export const configRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
name: configNameSchema,
|
||||
}),
|
||||
})
|
||||
)
|
||||
.output(z.object({ message: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
if (input.name.toLowerCase() === 'default') {
|
||||
Consola.error('Rejected config deletion because default configuration can\'t be deleted');
|
||||
Consola.error("Rejected config deletion because default configuration can't be deleted");
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Default config can\'t be deleted',
|
||||
message: "Default config can't be deleted",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const configRouter = createTRPCRouter({
|
||||
// If the target is not in the list of files, return an error
|
||||
if (!matchedFile) {
|
||||
Consola.error(
|
||||
`Rejected config deletion request because config name '${input.name}' was not included in present configurations`,
|
||||
`Rejected config deletion request because config name '${input.name}' was not included in present configurations`
|
||||
);
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
@@ -64,9 +64,13 @@ export const configRouter = createTRPCRouter({
|
||||
z.object({
|
||||
name: configNameSchema,
|
||||
config: z.custom<ConfigType>((x) => !!x && typeof x === 'object'),
|
||||
}),
|
||||
create: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
if (input.create && configExists(input.name))
|
||||
throw new TRPCError({ message: 'Config already exists.', code: 'CONFLICT' });
|
||||
|
||||
Consola.info(`Saving updated configuration of '${input.name}' config.`);
|
||||
|
||||
const previousConfig = getConfig(input.name);
|
||||
@@ -96,16 +100,16 @@ export const configRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const previousApp = previousConfig.apps.find(
|
||||
(previousApp) => previousApp.id === app.id,
|
||||
(previousApp) => previousApp.id === app.id
|
||||
);
|
||||
|
||||
const previousProperty = previousApp?.integration?.properties.find(
|
||||
(previousProperty) => previousProperty.field === property.field,
|
||||
(previousProperty) => previousProperty.field === property.field
|
||||
);
|
||||
|
||||
if (property.value !== undefined && property.value !== null) {
|
||||
Consola.info(
|
||||
'Detected credential change of private secret. Value will be overwritten in configuration',
|
||||
'Detected credential change of private secret. Value will be overwritten in configuration'
|
||||
);
|
||||
return {
|
||||
field: property.field,
|
||||
@@ -168,13 +172,14 @@ export const configRouter = createTRPCRouter({
|
||||
path: '/configs/byName',
|
||||
tags: ['config'],
|
||||
deprecated: true,
|
||||
summary: 'Retrieve content of the JSON configuration. Deprecated because JSON will be removed in a future version and be replaced with a relational database.'
|
||||
}
|
||||
summary:
|
||||
'Retrieve content of the JSON configuration. Deprecated because JSON will be removed in a future version and be replaced with a relational database.',
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
name: configNameSchema,
|
||||
}),
|
||||
})
|
||||
)
|
||||
.output(z.custom<ConfigType>())
|
||||
.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 { eq } from 'drizzle-orm';
|
||||
import { type GetServerSidePropsContext, type NextApiRequest, type NextApiResponse } from 'next';
|
||||
import { type DefaultSession, type NextAuthOptions, getServerSession } from 'next-auth';
|
||||
import { type NextAuthOptions, getServerSession } from 'next-auth';
|
||||
import { Adapter } from 'next-auth/adapters';
|
||||
import { decode, encode } from 'next-auth/jwt';
|
||||
import Credentials from 'next-auth/providers/credentials';
|
||||
import { adapter, onCreateUser, providers } from '~/utils/auth';
|
||||
import EmptyNextAuthProvider from '~/utils/empty-provider';
|
||||
import { fromDate, generateSessionToken } from '~/utils/session';
|
||||
import { colorSchemeParser, signInSchema } from '~/validations/user';
|
||||
import { colorSchemeParser } from '~/validations/user';
|
||||
|
||||
import { db } from './db';
|
||||
import { users } from './db/schema';
|
||||
|
||||
/**
|
||||
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
||||
* object and keep type safety.
|
||||
*
|
||||
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
|
||||
*/
|
||||
declare module 'next-auth' {
|
||||
interface Session extends DefaultSession {
|
||||
user: DefaultSession['user'] & {
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
colorScheme: 'light' | 'dark' | 'environment';
|
||||
autoFocusSearch: boolean;
|
||||
language: string;
|
||||
// ...other properties
|
||||
// role: UserRole;
|
||||
};
|
||||
}
|
||||
|
||||
interface User {
|
||||
isAdmin: boolean;
|
||||
colorScheme: 'light' | 'dark' | 'environment';
|
||||
autoFocusSearch: boolean;
|
||||
language: string;
|
||||
// ...other properties
|
||||
// role: UserRole;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'next-auth/jwt' {
|
||||
interface JWT {
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = DrizzleAdapter(db);
|
||||
const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
||||
|
||||
/**
|
||||
@@ -63,6 +23,9 @@ export const constructAuthOptions = (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): NextAuthOptions => ({
|
||||
events: {
|
||||
createUser: onCreateUser,
|
||||
},
|
||||
callbacks: {
|
||||
async session({ session, user }) {
|
||||
if (session.user) {
|
||||
@@ -133,58 +96,7 @@ export const constructAuthOptions = (
|
||||
error: '/auth/login',
|
||||
},
|
||||
adapter: adapter as Adapter,
|
||||
providers: [
|
||||
Credentials({
|
||||
name: 'credentials',
|
||||
credentials: {
|
||||
name: {
|
||||
label: 'Username',
|
||||
type: 'text',
|
||||
},
|
||||
password: { label: 'Password', type: 'password' },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
const data = await signInSchema.parseAsync(credentials);
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
with: {
|
||||
settings: {
|
||||
columns: {
|
||||
colorScheme: true,
|
||||
language: true,
|
||||
autoFocusSearch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: eq(users.name, data.name),
|
||||
});
|
||||
|
||||
if (!user || !user.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Consola.log(`user ${user.name} is trying to log in. checking password...`);
|
||||
const isValidPassword = await bcrypt.compare(data.password, user.password);
|
||||
|
||||
if (!isValidPassword) {
|
||||
Consola.log(`password for user ${user.name} was incorrect`);
|
||||
return null;
|
||||
}
|
||||
|
||||
Consola.log(`user ${user.name} successfully authorized`);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
isAdmin: false,
|
||||
colorScheme: colorSchemeParser.parse(user.settings?.colorScheme),
|
||||
language: user.settings?.language ?? 'en',
|
||||
autoFocusSearch: user.settings?.autoFocusSearch ?? false,
|
||||
};
|
||||
},
|
||||
}),
|
||||
EmptyNextAuthProvider(),
|
||||
],
|
||||
providers: [...providers, EmptyNextAuthProvider()],
|
||||
jwt: {
|
||||
async encode(params) {
|
||||
if (!isCredentialsRequest(req)) {
|
||||
@@ -207,10 +119,12 @@ export const constructAuthOptions = (
|
||||
});
|
||||
|
||||
const isCredentialsRequest = (req: NextApiRequest): boolean => {
|
||||
const nextAuthQueryParams = req.query.nextauth as ['callback', 'credentials'];
|
||||
const nextAuthQueryParams = req.query.nextauth as string[];
|
||||
return (
|
||||
nextAuthQueryParams.includes('callback') &&
|
||||
nextAuthQueryParams.includes('credentials') &&
|
||||
(nextAuthQueryParams.includes('credentials') ||
|
||||
nextAuthQueryParams.includes('ldap') ||
|
||||
nextAuthQueryParams.includes('oidc')) &&
|
||||
req.method === 'POST'
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,9 @@ import { InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { index, int, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { type AdapterAccount } from 'next-auth/adapters';
|
||||
|
||||
export const users = sqliteTable('user', {
|
||||
// workaround for typescript check in adapter
|
||||
// preferably add email into credential login and make email non-nullable here
|
||||
export const _users = {
|
||||
id: text('id').notNull().primaryKey(),
|
||||
name: text('name'),
|
||||
email: text('email'),
|
||||
@@ -12,7 +14,9 @@ export const users = sqliteTable('user', {
|
||||
salt: text('salt'),
|
||||
isAdmin: int('is_admin', { mode: 'boolean' }).notNull().default(false),
|
||||
isOwner: int('is_owner', { mode: 'boolean' }).notNull().default(false),
|
||||
});
|
||||
};
|
||||
|
||||
export const users = sqliteTable('user', _users);
|
||||
|
||||
export const accounts = sqliteTable(
|
||||
'account',
|
||||
|
||||
@@ -22,6 +22,7 @@ export const boardNamespaces = [
|
||||
'modules/dashdot',
|
||||
'modules/overseerr',
|
||||
'modules/media-server',
|
||||
'modules/indexer-manager',
|
||||
'modules/common-media-cards',
|
||||
'modules/video-stream',
|
||||
'modules/media-requests-list',
|
||||
@@ -44,7 +45,7 @@ export const manageNamespaces = [
|
||||
'manage/users',
|
||||
'manage/users/invites',
|
||||
'manage/users/create',
|
||||
'manage/users/edit'
|
||||
'manage/users/edit',
|
||||
];
|
||||
export const loginNamespaces = ['authentication/login'];
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Icon, IconKey, IconPassword, IconUser } from '@tabler/icons-react';
|
||||
|
||||
import { Property } from 'csstype';
|
||||
|
||||
import { TileBaseType } from './tile';
|
||||
@@ -46,6 +45,7 @@ export type IntegrationType =
|
||||
| 'radarr'
|
||||
| 'sonarr'
|
||||
| 'lidarr'
|
||||
| 'prowlarr'
|
||||
| 'sabnzbd'
|
||||
| 'jellyseerr'
|
||||
| 'overseerr'
|
||||
@@ -87,6 +87,7 @@ export const integrationFieldProperties: {
|
||||
lidarr: ['apiKey'],
|
||||
radarr: ['apiKey'],
|
||||
sonarr: ['apiKey'],
|
||||
prowlarr: ['apiKey'],
|
||||
sabnzbd: ['apiKey'],
|
||||
readarr: ['apiKey'],
|
||||
overseerr: ['apiKey'],
|
||||
@@ -99,7 +100,7 @@ export const integrationFieldProperties: {
|
||||
plex: ['apiKey'],
|
||||
pihole: ['apiKey'],
|
||||
adGuardHome: ['username', 'password'],
|
||||
homeAssistant: ['apiKey']
|
||||
homeAssistant: ['apiKey'],
|
||||
};
|
||||
|
||||
export type IntegrationFieldDefinitionType = {
|
||||
|
||||
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' });
|
||||
},
|
||||
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' });
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import dnsHoleControls from './dnshole/DnsHoleControls';
|
||||
import dnsHoleSummary from './dnshole/DnsHoleSummary';
|
||||
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
|
||||
import iframe from './iframe/IFrameTile';
|
||||
import indexerManager from './indexer-manager/IndexerManagerTile';
|
||||
import mediaRequestsList from './media-requests/MediaRequestListTile';
|
||||
import mediaRequestsStats from './media-requests/MediaRequestStatsTile';
|
||||
import mediaServer from './media-server/MediaServerTile';
|
||||
@@ -20,6 +21,7 @@ import weather from './weather/WeatherTile';
|
||||
|
||||
export default {
|
||||
calendar,
|
||||
'indexer-manager': indexerManager,
|
||||
dashdot,
|
||||
usenet,
|
||||
weather,
|
||||
|
||||
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) {
|
||||
return /^https?:\/\//.test(url);
|
||||
},
|
||||
}).extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
target: { default: null }
|
||||
}
|
||||
}
|
||||
}),
|
||||
StarterKit,
|
||||
Table.configure({
|
||||
|
||||
@@ -16,6 +16,11 @@ const definition = defineWidget({
|
||||
defaultValue: 'sun.sun',
|
||||
info: true,
|
||||
},
|
||||
appendUnit: {
|
||||
type: 'switch',
|
||||
defaultValue: false,
|
||||
info: true,
|
||||
},
|
||||
automationId: {
|
||||
type: 'text',
|
||||
info: true,
|
||||
@@ -25,6 +30,11 @@ const definition = defineWidget({
|
||||
type: 'text',
|
||||
defaultValue: 'Sun',
|
||||
},
|
||||
displayFriendlyName: {
|
||||
type: 'switch',
|
||||
defaultValue: false,
|
||||
info: true,
|
||||
},
|
||||
},
|
||||
gridstack: {
|
||||
minWidth: 1,
|
||||
@@ -58,6 +68,14 @@ function EntityStateTile({ widget }: SmartHomeEntityStateWidgetProps) {
|
||||
},
|
||||
);
|
||||
|
||||
const attribute = (widget.properties.appendUnit && data?.attributes.unit_of_measurement ?
|
||||
" " + data?.attributes.unit_of_measurement : ""
|
||||
)
|
||||
|
||||
const displayName = (widget.properties.displayFriendlyName && data?.attributes.friendly_name ?
|
||||
data?.attributes.friendly_name : widget.properties.displayName
|
||||
)
|
||||
|
||||
const { mutateAsync: mutateTriggerAutomationAsync } = api.smartHomeEntityState.triggerAutomation.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.smartHomeEntityState.invalidate();
|
||||
@@ -101,6 +119,7 @@ function EntityStateTile({ widget }: SmartHomeEntityStateWidgetProps) {
|
||||
dataComponent = (
|
||||
<Text align="center">
|
||||
{data?.state}
|
||||
{attribute}
|
||||
{isLoading && <Loader ml="xs" size={10} />}
|
||||
</Text>
|
||||
);
|
||||
@@ -118,7 +137,7 @@ function EntityStateTile({ widget }: SmartHomeEntityStateWidgetProps) {
|
||||
w="100%">
|
||||
<Stack align="center" spacing={3}>
|
||||
<Text align="center" weight="bold" size="lg">
|
||||
{widget.properties.displayName}
|
||||
{displayName}
|
||||
</Text>
|
||||
{dataComponent}
|
||||
</Stack>
|
||||
|
||||
@@ -38,6 +38,9 @@ describe('login page', () => {
|
||||
redirectAfterLogin: null,
|
||||
isDemo: false,
|
||||
_i18Next: 'hello',
|
||||
oidcAutoLogin: null,
|
||||
oidcProviderName: null,
|
||||
providers: undefined
|
||||
},
|
||||
});
|
||||
|
||||
@@ -75,6 +78,9 @@ describe('login page', () => {
|
||||
redirectAfterLogin: '/manage/users/create',
|
||||
isDemo: false,
|
||||
_i18Next: 'hello',
|
||||
oidcAutoLogin: null,
|
||||
oidcProviderName: null,
|
||||
providers: undefined
|
||||
},
|
||||
});
|
||||
|
||||
@@ -112,6 +118,9 @@ describe('login page', () => {
|
||||
redirectAfterLogin: null,
|
||||
isDemo: false,
|
||||
_i18Next: 'hello',
|
||||
oidcAutoLogin: null,
|
||||
oidcProviderName: null,
|
||||
providers: undefined
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"target": "es2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
|
||||
299
yarn.lock
299
yarn.lock
@@ -22,34 +22,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@auth/core@npm:0.18.1":
|
||||
version: 0.18.1
|
||||
resolution: "@auth/core@npm:0.18.1"
|
||||
dependencies:
|
||||
"@panva/hkdf": ^1.1.1
|
||||
cookie: 0.5.0
|
||||
jose: ^5.1.0
|
||||
oauth4webapi: ^2.3.0
|
||||
preact: 10.11.3
|
||||
preact-render-to-string: 5.2.3
|
||||
peerDependencies:
|
||||
nodemailer: ^6.8.0
|
||||
peerDependenciesMeta:
|
||||
nodemailer:
|
||||
optional: true
|
||||
checksum: 46ae80e621e03d9206cc9a5e37941df92207e58298f423ec71ae2b8d3492d86f14d5e024ba30c5a905675c451688d212d389b580748f3a176ec0ddcd3872291a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@auth/drizzle-adapter@npm:^0.3.2":
|
||||
version: 0.3.6
|
||||
resolution: "@auth/drizzle-adapter@npm:0.3.6"
|
||||
dependencies:
|
||||
"@auth/core": 0.18.1
|
||||
checksum: c80abc825ab15645f39ad4fd630ca81caf18880aca32f8df030a072dfb7f5222d1fe4396713041bf24e7252c8478a09be81ac4f921652497319acf30e138f4ec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.22.13":
|
||||
version: 7.22.13
|
||||
resolution: "@babel/code-frame@npm:7.22.13"
|
||||
@@ -1033,6 +1005,95 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ldapjs/asn1@npm:2.0.0, @ldapjs/asn1@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@ldapjs/asn1@npm:2.0.0"
|
||||
checksum: b9957b47b14ef0a24fa5275849b624f8a1a7708c2f37b0b7ff278062527a7c93a885c9a73462c3bba4a9e182bd0766422f08597361cdbf3ecafd7dfb478ab490
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ldapjs/asn1@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@ldapjs/asn1@npm:1.2.0"
|
||||
checksum: 720b65fd825b414f672264c19edf2b67f643bd655ac9dae761394f40e332c68fbe7f442046daf88a00a656ca2cbbfe91c0435fc59c9b7c301770ea0d2606b89a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ldapjs/attribute@npm:1.0.0, @ldapjs/attribute@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@ldapjs/attribute@npm:1.0.0"
|
||||
dependencies:
|
||||
"@ldapjs/asn1": 2.0.0
|
||||
"@ldapjs/protocol": ^1.2.1
|
||||
process-warning: ^2.1.0
|
||||
checksum: 887665a3067deebbfea7760befc535f94205f87cece0f164f9ddc2f3f5b0daa136a0ede4520fa37aa9d30af025cb23023a155473bbc61916aa39da2ad697c7f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ldapjs/change@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@ldapjs/change@npm:1.0.0"
|
||||
dependencies:
|
||||
"@ldapjs/asn1": 2.0.0
|
||||
"@ldapjs/attribute": 1.0.0
|
||||
checksum: 5f28d8e904fe47cbaff225d9696d35ee78f1f648e2aedab9aebe67c0b19df4a9b0224bf2ac9a8ab2d1dab00e69eaff9f17be5532af2f32862e27a973228d83eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ldapjs/controls@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "@ldapjs/controls@npm:2.1.0"
|
||||
dependencies:
|
||||
"@ldapjs/asn1": ^1.2.0
|
||||
"@ldapjs/protocol": ^1.2.1
|
||||
checksum: b61a69ddf0634ea6bbc1a32691fa19ee92aa2efe17aeae77ea261b5b16cf6102c36ed71ef0ce038ec74fe7751917c0946862fdabe328086b7561b6e6453ef794
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ldapjs/dn@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@ldapjs/dn@npm:1.1.0"
|
||||
dependencies:
|
||||
"@ldapjs/asn1": 2.0.0
|
||||
process-warning: ^2.1.0
|
||||
checksum: 716e408c9f8ea1d1f14c512a1ecbc3271d7873da1aee788bfa6548a47290fecefd9ea2039f1f9f9238cba8072ae798c4e4b4da5e457ee24b68e94572665f711f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ldapjs/filter@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@ldapjs/filter@npm:2.1.1"
|
||||
dependencies:
|
||||
"@ldapjs/asn1": 2.0.0
|
||||
"@ldapjs/protocol": ^1.2.1
|
||||
process-warning: ^2.1.0
|
||||
checksum: e87c698fe7921969a751479b435a58f8202ebbe48420a3705dd47180b33ae39fcbe1451640c58fb94f80b4a96efa91d99ec91e6dc6d7be96b7bc3cc469506ba8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ldapjs/messages@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "@ldapjs/messages@npm:1.3.0"
|
||||
dependencies:
|
||||
"@ldapjs/asn1": ^2.0.0
|
||||
"@ldapjs/attribute": ^1.0.0
|
||||
"@ldapjs/change": ^1.0.0
|
||||
"@ldapjs/controls": ^2.1.0
|
||||
"@ldapjs/dn": ^1.1.0
|
||||
"@ldapjs/filter": ^2.1.1
|
||||
"@ldapjs/protocol": ^1.2.1
|
||||
process-warning: ^2.2.0
|
||||
checksum: e7f1994db976456546769d72b2efba18c93e9201c81050b52479575bf72bac42312c6b817e886ac315caf592b00d2f0d3407fcc4eea58ff65c8bd18211e5b458
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ldapjs/protocol@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "@ldapjs/protocol@npm:1.2.1"
|
||||
checksum: 3e26f3fc642897ae1448a5a172839ab368fe72e05b9eaf36e16fe6dd4c3c93ce298ab4e3907b3eda9c3911e018d617777b79071dfbb3b813add56b046aea48dc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/core@npm:^6.0.0":
|
||||
version: 6.0.21
|
||||
resolution: "@mantine/core@npm:6.0.21"
|
||||
@@ -1504,7 +1565,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@panva/hkdf@npm:^1.0.2, @panva/hkdf@npm:^1.1.1":
|
||||
"@panva/hkdf@npm:^1.0.2":
|
||||
version: 1.1.1
|
||||
resolution: "@panva/hkdf@npm:1.1.1"
|
||||
checksum: f0dd12903751d8792420353f809ed3c7de860cf506399759fff5f59f7acfef8a77e2b64012898cee7e5b047708fa0bd91dff5ef55a502bf8ea11aad9842160da
|
||||
@@ -3286,6 +3347,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ldapjs@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "@types/ldapjs@npm:3.0.2"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: 0839acb3c46aa231577266c46700b44cfeb5cc77cfb854be6dac25bf1346cd0b5c83e3671fd6a78769c7702a1eb610d5f08b68e2583bb5c13d214eb0558c3d36
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mime@npm:*":
|
||||
version: 3.0.4
|
||||
resolution: "@types/mime@npm:3.0.4"
|
||||
@@ -3919,6 +3989,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"abstract-logging@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "abstract-logging@npm:2.0.1"
|
||||
checksum: 6967d15e5abbafd17f56eaf30ba8278c99333586fa4f7935fd80e93cfdc006c37fcc819c5d63ee373a12e6cb2d0417f7c3c6b9e42b957a25af9937d26749415e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"accepts@npm:^1.3.7":
|
||||
version: 1.3.8
|
||||
resolution: "accepts@npm:1.3.8"
|
||||
@@ -4207,6 +4284,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"assert-plus@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "assert-plus@npm:1.0.0"
|
||||
checksum: 19b4340cb8f0e6a981c07225eacac0e9d52c2644c080198765d63398f0075f83bbc0c8e95474d54224e297555ad0d631c1dcd058adb1ddc2437b41a6b424ac64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"assertion-error@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "assertion-error@npm:1.1.0"
|
||||
@@ -4309,6 +4393,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"backoff@npm:^2.5.0":
|
||||
version: 2.5.0
|
||||
resolution: "backoff@npm:2.5.0"
|
||||
dependencies:
|
||||
precond: 0.2
|
||||
checksum: ccdcf2a26acd9379d0d4f09e3fb3b7ee34dee94f07ab74d1e38b38f89a3675d9f3cbebb142d9c61c655f4c9eb63f1d6ec28cebeb3dc9215efd8fe7cef92725b9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"balanced-match@npm:^1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "balanced-match@npm:1.0.2"
|
||||
@@ -4904,13 +4997,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie@npm:0.5.0, cookie@npm:^0.5.0":
|
||||
version: 0.5.0
|
||||
resolution: "cookie@npm:0.5.0"
|
||||
checksum: 1f4bd2ca5765f8c9689a7e8954183f5332139eb72b6ff783d8947032ec1fdf43109852c178e21a953a30c0dd42257828185be01b49d1eb1a67fd054ca588a180
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie@npm:^0.4.0":
|
||||
version: 0.4.2
|
||||
resolution: "cookie@npm:0.4.2"
|
||||
@@ -4918,6 +5004,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie@npm:^0.5.0":
|
||||
version: 0.5.0
|
||||
resolution: "cookie@npm:0.5.0"
|
||||
checksum: 1f4bd2ca5765f8c9689a7e8954183f5332139eb72b6ff783d8947032ec1fdf43109852c178e21a953a30c0dd42257828185be01b49d1eb1a67fd054ca588a180
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie@npm:~0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "cookie@npm:0.6.0"
|
||||
@@ -4978,6 +5071,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"core-util-is@npm:1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "core-util-is@npm:1.0.2"
|
||||
checksum: 7a4c925b497a2c91421e25bf76d6d8190f0b2359a9200dbeed136e63b2931d6294d3b1893eda378883ed363cd950f44a12a401384c609839ea616befb7927dab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"core-util-is@npm:~1.0.0":
|
||||
version: 1.0.3
|
||||
resolution: "core-util-is@npm:1.0.3"
|
||||
@@ -6481,6 +6581,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"extsprintf@npm:^1.2.0":
|
||||
version: 1.4.1
|
||||
resolution: "extsprintf@npm:1.4.1"
|
||||
checksum: a2f29b241914a8d2bad64363de684821b6b1609d06ae68d5b539e4de6b28659715b5bea94a7265201603713b7027d35399d10b0548f09071c5513e65e8323d33
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3":
|
||||
version: 3.1.3
|
||||
resolution: "fast-deep-equal@npm:3.1.3"
|
||||
@@ -7276,7 +7383,6 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "homarr@workspace:."
|
||||
dependencies:
|
||||
"@auth/drizzle-adapter": ^0.3.2
|
||||
"@ctrl/deluge": ^4.1.0
|
||||
"@ctrl/qbittorrent": ^6.0.0
|
||||
"@ctrl/shared-torrent": ^4.1.1
|
||||
@@ -7327,6 +7433,7 @@ __metadata:
|
||||
"@types/better-sqlite3": ^7.6.5
|
||||
"@types/cookies": ^0.7.7
|
||||
"@types/dockerode": ^3.3.9
|
||||
"@types/ldapjs": ^3.0.2
|
||||
"@types/node": 18.17.8
|
||||
"@types/prismjs": ^1.26.0
|
||||
"@types/react": ^18.2.11
|
||||
@@ -7370,9 +7477,8 @@ __metadata:
|
||||
i18next: ^22.5.1
|
||||
immer: ^10.0.2
|
||||
js-file-download: ^0.4.12
|
||||
ldapjs: ^3.0.5
|
||||
mantine-react-table: ^1.3.4
|
||||
moment: ^2.29.4
|
||||
moment-timezone: ^0.5.43
|
||||
next: 13.4.12
|
||||
next-auth: ^4.23.0
|
||||
next-i18next: ^14.0.0
|
||||
@@ -8188,13 +8294,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jose@npm:^5.1.0":
|
||||
version: 5.1.1
|
||||
resolution: "jose@npm:5.1.1"
|
||||
checksum: 3a18d85dd1ed0e7746c67cba65a95ee972f20b363ceb99a9d75b870beb34942089cfca6249c4a50a79bc854c5a052f1be39e814c42b0f00f9358e902ce706e8d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-file-download@npm:^0.4.12":
|
||||
version: 0.4.12
|
||||
resolution: "js-file-download@npm:0.4.12"
|
||||
@@ -8398,6 +8497,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ldapjs@npm:^3.0.5":
|
||||
version: 3.0.7
|
||||
resolution: "ldapjs@npm:3.0.7"
|
||||
dependencies:
|
||||
"@ldapjs/asn1": ^2.0.0
|
||||
"@ldapjs/attribute": ^1.0.0
|
||||
"@ldapjs/change": ^1.0.0
|
||||
"@ldapjs/controls": ^2.1.0
|
||||
"@ldapjs/dn": ^1.1.0
|
||||
"@ldapjs/filter": ^2.1.1
|
||||
"@ldapjs/messages": ^1.3.0
|
||||
"@ldapjs/protocol": ^1.2.1
|
||||
abstract-logging: ^2.0.1
|
||||
assert-plus: ^1.0.0
|
||||
backoff: ^2.5.0
|
||||
once: ^1.4.0
|
||||
vasync: ^2.2.1
|
||||
verror: ^1.10.1
|
||||
checksum: 4c0c4aeb5a0e22d0b1cba3779663472d8ebe6bc0fed5e56d6e29ac15b7f9e567e673c8764d0e51ca52eab48eef2024561a3553d6c804b11a260a893c18bd8df7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"levn@npm:^0.4.1":
|
||||
version: 0.4.1
|
||||
resolution: "levn@npm:0.4.1"
|
||||
@@ -8960,22 +9081,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"moment-timezone@npm:^0.5.43":
|
||||
version: 0.5.43
|
||||
resolution: "moment-timezone@npm:0.5.43"
|
||||
dependencies:
|
||||
moment: ^2.29.4
|
||||
checksum: 8075c897ed8a044f992ef26fe8cdbcad80caf974251db424cae157473cca03be2830de8c74d99341b76edae59f148c9d9d19c1c1d9363259085688ec1cf508d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"moment@npm:^2.29.4":
|
||||
version: 2.29.4
|
||||
resolution: "moment@npm:2.29.4"
|
||||
checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mpd-parser@npm:^1.0.1, mpd-parser@npm:^1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "mpd-parser@npm:1.2.2"
|
||||
@@ -9341,13 +9446,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oauth4webapi@npm:^2.3.0":
|
||||
version: 2.4.0
|
||||
resolution: "oauth4webapi@npm:2.4.0"
|
||||
checksum: 9e6d5be3966013aa9dd61781032a6bd07a63166a9819f2fc0d622d33b23221ea39ae25334a4bde9eba4623e576972d367b196e3b5d3facff75002125c510b672
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oauth@npm:^0.9.15":
|
||||
version: 0.9.15
|
||||
resolution: "oauth@npm:0.9.15"
|
||||
@@ -9801,17 +9899,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact-render-to-string@npm:5.2.3":
|
||||
version: 5.2.3
|
||||
resolution: "preact-render-to-string@npm:5.2.3"
|
||||
dependencies:
|
||||
pretty-format: ^3.8.0
|
||||
peerDependencies:
|
||||
preact: ">=10"
|
||||
checksum: 6e46288d8956adde35b9fe3a21aecd9dea29751b40f0f155dea62f3896f27cb8614d457b32f48d33909d2da81135afcca6c55077520feacd7d15164d1371fb44
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact-render-to-string@npm:^5.1.19":
|
||||
version: 5.2.6
|
||||
resolution: "preact-render-to-string@npm:5.2.6"
|
||||
@@ -9823,13 +9910,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact@npm:10.11.3":
|
||||
version: 10.11.3
|
||||
resolution: "preact@npm:10.11.3"
|
||||
checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact@npm:^10.6.3":
|
||||
version: 10.19.2
|
||||
resolution: "preact@npm:10.19.2"
|
||||
@@ -9859,6 +9939,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"precond@npm:0.2":
|
||||
version: 0.2.3
|
||||
resolution: "precond@npm:0.2.3"
|
||||
checksum: c613e7d68af3e0b43a294a994bf067cc2bc44b03fd17bc4fb133e30617a4f5b49414b08e9b392d52d7c6822d8a71f66a7fe93a8a1e7d02240177202cff3f63ef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prelude-ls@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "prelude-ls@npm:1.2.1"
|
||||
@@ -9941,6 +10028,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"process-warning@npm:^2.1.0, process-warning@npm:^2.2.0":
|
||||
version: 2.3.2
|
||||
resolution: "process-warning@npm:2.3.2"
|
||||
checksum: cbeddc85d3963eccd6578b1eea5ba981383d1ec688d6e4ba5bf0ca6662d094c024b44dfcb1c530662c7694b68fe09fd95fa0269a1309090d793008f4553e7784
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"process@npm:^0.11.10":
|
||||
version: 0.11.10
|
||||
resolution: "process@npm:0.11.10"
|
||||
@@ -12520,6 +12614,37 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vasync@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "vasync@npm:2.2.1"
|
||||
dependencies:
|
||||
verror: 1.10.0
|
||||
checksum: dca14090436f1b30d4887737af47bc8333795a6d45e520e583ca2c4476d841bf68606cbc79071cfd980e3e42e630736d66a598b9100a505663442ae2e7c2f92f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"verror@npm:1.10.0":
|
||||
version: 1.10.0
|
||||
resolution: "verror@npm:1.10.0"
|
||||
dependencies:
|
||||
assert-plus: ^1.0.0
|
||||
core-util-is: 1.0.2
|
||||
extsprintf: ^1.2.0
|
||||
checksum: c431df0bedf2088b227a4e051e0ff4ca54df2c114096b0c01e1cbaadb021c30a04d7dd5b41ab277bcd51246ca135bf931d4c4c796ecae7a4fef6d744ecef36ea
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"verror@npm:^1.10.1":
|
||||
version: 1.10.1
|
||||
resolution: "verror@npm:1.10.1"
|
||||
dependencies:
|
||||
assert-plus: ^1.0.0
|
||||
core-util-is: 1.0.2
|
||||
extsprintf: ^1.2.0
|
||||
checksum: 690a8d6ad5a4001672290e9719e3107c86269bc45fe19f844758eecf502e59f8aa9631b19b839f6d3dea562334884d22d1eb95ae7c863032075a9212c889e116
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"video.js@npm:^7 || ^8, video.js@npm:^8.0.3":
|
||||
version: 8.6.1
|
||||
resolution: "video.js@npm:8.6.1"
|
||||
|
||||
Reference in New Issue
Block a user