diff --git a/packages/ui/public/locales/en.json b/packages/ui/public/locales/en.json
index ea1bd50d4..3fb6fee73 100644
--- a/packages/ui/public/locales/en.json
+++ b/packages/ui/public/locales/en.json
@@ -47,7 +47,7 @@
"app-store.category.media": "Media",
"app-store.category.networking": "Networking",
"app-store.category.social": "Social",
- "app-store.description": "Your app update settings",
+ "app-store.description": "Registries, preferences",
"app-store.discover.temporarily-unavailable-description": "Browse categories above or search to find apps",
"app-store.discover.temporarily-unavailable-title": "Featured content temporarily unavailable",
"app-store.menu.community-app-stores": "Community App Stores",
@@ -961,6 +961,14 @@
"search": "Search",
"settings": "Settings",
"settings.app-store-preferences.title": "App Store Preferences",
+ "settings.app-store-preferences.registry-credentials.title": "Private Registry Credentials",
+ "settings.app-store-preferences.registry-credentials.description": "Login credentials for private container registries (e.g. ghcr.io). Used when installing community apps that require private images.",
+ "settings.app-store-preferences.registry-credentials.add-button": "Add registry",
+ "settings.app-store-preferences.registry-credentials.registry-placeholder": "Registry host (e.g. ghcr.io)",
+ "settings.app-store-preferences.registry-credentials.username-placeholder": "Username",
+ "settings.app-store-preferences.registry-credentials.token-placeholder": "Token / Password",
+ "settings.app-store-preferences.registry-credentials.save": "Save",
+ "settings.app-store-preferences.registry-credentials.remove": "Remove",
"settings.contact-support": "Need help? Contact support.",
"settings.file-sharing": "File sharing",
"settings.file-sharing.add-folder": "Add",
diff --git a/packages/ui/src/routes/settings/_components/app-store-preferences-content.tsx b/packages/ui/src/routes/settings/_components/app-store-preferences-content.tsx
index 84e0183c3..532633c5a 100644
--- a/packages/ui/src/routes/settings/_components/app-store-preferences-content.tsx
+++ b/packages/ui/src/routes/settings/_components/app-store-preferences-content.tsx
@@ -1,71 +1,115 @@
import {useState} from 'react'
-import {TbChevronRight} from 'react-icons/tb'
+import {useTranslation} from 'react-i18next'
+import {TbLock, TbPlus, TbTrash} from 'react-icons/tb'
-import {ChevronDown} from '@/components/chevron-down'
import {Button} from '@/components/ui/button'
-import {
- DropdownMenu,
- DropdownMenuCheckboxItem,
- DropdownMenuContent,
- DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu'
+import {Input, PasswordInput} from '@/components/ui/input'
import {listClass, listItemClass} from '@/components/ui/list'
-import {SegmentedControl} from '@/components/ui/segmented-control'
-import {Switch} from '@/components/ui/switch'
+import {trpcReact} from '@/trpc/trpc'
export function AppStorePreferencesContent() {
- const tabs = [
- {id: 'auto-update', label: 'Auto-update'},
- {id: 'notifications', label: 'Notifications'},
- {id: 'uninstall', label: 'Uninstall'},
- ]
- const [activeTab, setActiveTab] = useState(tabs[0].id)
+ const {t} = useTranslation()
+ const utils = trpcReact.useUtils()
+
+ const credentialsQ = trpcReact.appStore.getRegistryCredentials.useQuery()
+ const setMut = trpcReact.appStore.setRegistryCredential.useMutation({
+ onSuccess: () => utils.appStore.getRegistryCredentials.invalidate(),
+ })
+ const removeMut = trpcReact.appStore.removeRegistryCredential.useMutation({
+ onSuccess: () => utils.appStore.getRegistryCredentials.invalidate(),
+ })
+
+ const [showForm, setShowForm] = useState(false)
+ const [registry, setRegistry] = useState('')
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+
+ const resetForm = () => {
+ setRegistry('')
+ setUsername('')
+ setPassword('')
+ setShowForm(false)
+ }
+
+ const handleSave = async (e: React.FormEvent) => {
+ e.preventDefault()
+ await setMut.mutateAsync({registry: registry.trim(), username: username.trim(), password})
+ resetForm()
+ }
+
+ const credentials = credentialsQ.data ?? []
return (
- <>
-
-
-
- Single value selector
-
-
-
-
-
- English
- French
-
-
-
-
+
+
+
+
+ {t('settings.app-store-preferences.registry-credentials.title')}
+
+
+ {t('settings.app-store-preferences.registry-credentials.description')}
+
-
-
-
-
-
-
- Lighting node
-
-
-
-
Lighting node
-
+
+ {credentials.length > 0 && (
+
+ {credentials.map(({registry: reg}) => (
+
+ {reg}
+
+
+ ))}
-
- >
+ )}
+
+ {!showForm && (
+
+ )}
+
+ {showForm && (
+
+ )}
+
)
}
diff --git a/packages/ui/src/routes/settings/_components/settings-content-mobile.tsx b/packages/ui/src/routes/settings/_components/settings-content-mobile.tsx
index 1ca68e36d..9805944e6 100644
--- a/packages/ui/src/routes/settings/_components/settings-content-mobile.tsx
+++ b/packages/ui/src/routes/settings/_components/settings-content-mobile.tsx
@@ -10,6 +10,7 @@ import {
TbServer,
TbSettingsMinus,
TbShare,
+ TbShoppingBag,
TbTool,
TbUser,
TbWifi,
@@ -210,12 +211,12 @@ export function SettingsContentMobile() {
description={t('backups-description')}
onClick={() => navigate('backups')}
/>
- {/*
navigate(linkToDialog('app-store-preferences'))}
- /> */}
+ />
- {/*
+
navigate(linkToDialog('app-store-preferences'))}>
{t('preferences')}
- */}
+
navigate('troubleshoot')}>
{t('troubleshoot')}
diff --git a/packages/umbreld/package-lock.json b/packages/umbreld/package-lock.json
index eda46097a..ff453eef3 100644
--- a/packages/umbreld/package-lock.json
+++ b/packages/umbreld/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "umbreld",
- "version": "1.5.0",
+ "version": "1.7.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "umbreld",
- "version": "1.5.0",
+ "version": "1.7.1",
"license": "PolyForm Noncommercial License 1.0.0",
"dependencies": {
"@homebridge/dbus-native": "github:getumbrel/dbus-native#types",
diff --git a/packages/umbreld/source/modules/apps/routes.ts b/packages/umbreld/source/modules/apps/routes.ts
index 613d8f2b7..dd7ec0b27 100644
--- a/packages/umbreld/source/modules/apps/routes.ts
+++ b/packages/umbreld/source/modules/apps/routes.ts
@@ -1,7 +1,10 @@
import z from 'zod'
+import fse from 'fs-extra'
import {router, privateProcedure} from '../server/trpc/trpc.js'
+const DOCKER_CONFIG_PATH = '/root/.docker/config.json'
+
export const appStore = router({
// Returns the app store registry
registry: privateProcedure.query(async ({ctx}) => ctx.appStore.registry()),
@@ -23,6 +26,54 @@ export const appStore = router({
}),
)
.mutation(async ({ctx, input}) => ctx.appStore.removeRepository(input.url)),
+
+ // List saved private registry hostnames (no passwords returned)
+ getRegistryCredentials: privateProcedure.query(async () => {
+ try {
+ const config = await fse.readJson(DOCKER_CONFIG_PATH)
+ const auths: Record = config?.auths ?? {}
+ return Object.keys(auths)
+ .filter((registry) => !!auths[registry]?.auth)
+ .map((registry) => ({registry}))
+ } catch {
+ return []
+ }
+ }),
+
+ // Save (add or update) credentials for a private registry
+ setRegistryCredential: privateProcedure
+ .input(
+ z.object({
+ registry: z.string().min(1),
+ username: z.string().min(1),
+ password: z.string().min(1),
+ }),
+ )
+ .mutation(async ({input}) => {
+ const config = (await fse.pathExists(DOCKER_CONFIG_PATH))
+ ? await fse.readJson(DOCKER_CONFIG_PATH)
+ : {}
+ config.auths = config.auths ?? {}
+ config.auths[input.registry] = {
+ auth: Buffer.from(`${input.username}:${input.password}`).toString('base64'),
+ }
+ await fse.outputJson(DOCKER_CONFIG_PATH, config, {spaces: 2})
+ return true
+ }),
+
+ // Remove credentials for a registry
+ removeRegistryCredential: privateProcedure
+ .input(z.object({registry: z.string().min(1)}))
+ .mutation(async ({input}) => {
+ try {
+ const config = await fse.readJson(DOCKER_CONFIG_PATH)
+ if (config?.auths?.[input.registry]) {
+ delete config.auths[input.registry]
+ await fse.outputJson(DOCKER_CONFIG_PATH, config, {spaces: 2})
+ }
+ } catch {}
+ return true
+ }),
})
export const apps = router({
diff --git a/packages/umbreld/source/modules/utilities/docker-pull.ts b/packages/umbreld/source/modules/utilities/docker-pull.ts
index 975e206dc..415e92014 100644
--- a/packages/umbreld/source/modules/utilities/docker-pull.ts
+++ b/packages/umbreld/source/modules/utilities/docker-pull.ts
@@ -1,17 +1,40 @@
import Dockerode from 'dockerode'
+import fse from 'fs-extra'
const docker = new Dockerode()
const DOWNLOADING_PERCENT = 0.75
const EXTRACTING_PERCENT = 0.25
+async function getAuthConfig(image: string): Promise