fix: container install flow, filebrowser auth, AppCard enrichment
Some checks failed
Build Archipelago ISO / build-iso (push) Has been cancelled
Some checks failed
Build Archipelago ISO / build-iso (push) Has been cancelled
- Fix .198-style fresh installs: systemd service ExecStartPre creates /run/user/1000, enable podman.socket, chmod 644 /etc/hosts - Filebrowser: add /data volume for database (fixes read-only crash), secure auth with random password via backend RPC (no more admin/admin) - AppCard: enrich installing state with marketplace metadata (icon, title, description, tier badge, author, version) - Registry: btcpayserver 1.13.5 → 1.13.7, images mirrored - ReadWritePaths: add home container paths for rootless podman Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,19 +40,18 @@ describe('FileBrowserClient', () => {
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
it('authenticates and stores token', async () => {
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse('"jwt-token-123"'))
|
||||
it('authenticates via backend RPC and stores token', async () => {
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ result: { token: 'jwt-token-123' } }))
|
||||
|
||||
// We need a fresh instance to test login — use the exported singleton
|
||||
const result = await fileBrowserClient.login('admin', 'admin')
|
||||
const result = await fileBrowserClient.login()
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(fileBrowserClient.isAuthenticated).toBe(true)
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/app/filebrowser/api/login'),
|
||||
'/rpc/v1',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username: 'admin', password: 'admin' }),
|
||||
body: JSON.stringify({ method: 'app.filebrowser-token' }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -60,7 +59,7 @@ describe('FileBrowserClient', () => {
|
||||
it('returns false on failed login', async () => {
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse(null, 403))
|
||||
|
||||
const result = await fileBrowserClient.login('admin', 'wrong')
|
||||
const result = await fileBrowserClient.login()
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
@@ -52,20 +52,21 @@ class FileBrowserClient {
|
||||
return match ? match[1]! : null
|
||||
}
|
||||
|
||||
async login(username = 'admin', password = 'admin'): Promise<boolean> {
|
||||
async login(): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`${this.baseUrl}/api/login`, {
|
||||
// Get a filebrowser JWT via the authenticated backend (no credentials exposed to browser)
|
||||
const rpcRes = await fetch('/rpc/v1', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
body: JSON.stringify({ method: 'app.filebrowser-token' }),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
if (!res.ok) return false
|
||||
const text = await res.text()
|
||||
// FileBrowser returns the JWT as a plain string (possibly quoted)
|
||||
const token = text.replace(/^"|"$/g, '')
|
||||
// Store token as cookie — the only auth mechanism we use
|
||||
if (!rpcRes.ok) return false
|
||||
const rpcData = await rpcRes.json()
|
||||
const token = rpcData?.result?.token
|
||||
if (!token) return false
|
||||
|
||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString()
|
||||
// Only set Secure flag on HTTPS — on HTTP it silently prevents the cookie from being stored
|
||||
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
||||
document.cookie = `auth=${token}; path=/app/filebrowser; SameSite=Lax${secure}; expires=${expires}`
|
||||
this._authenticated = true
|
||||
|
||||
@@ -107,6 +107,7 @@ export interface Manifest {
|
||||
'donation-url': string | null
|
||||
author?: string
|
||||
website?: string
|
||||
tier?: string
|
||||
interfaces?: {
|
||||
main?: {
|
||||
ui?: string
|
||||
|
||||
@@ -39,43 +39,51 @@
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<img
|
||||
:src="pkg['static-files']?.icon || `/assets/img/app-icons/${id}.png`"
|
||||
:alt="pkg.manifest?.title || String(id)"
|
||||
class="w-16 h-16 rounded-lg object-cover bg-white/10"
|
||||
:src="icon"
|
||||
:alt="title"
|
||||
class="w-14 h-14 rounded-lg object-cover bg-white/10"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div class="flex-1 min-w-0 overflow-hidden">
|
||||
<h3 class="text-lg font-semibold text-white mb-1 truncate" :title="pkg.manifest.title">
|
||||
{{ pkg.manifest.title }}
|
||||
</h3>
|
||||
<p class="text-sm text-white/70 mb-2 truncate">
|
||||
{{ pkg.manifest?.description?.short || '' }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<h3 class="text-lg font-semibold text-white truncate" :title="title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium"
|
||||
:class="getStatusClass(pkg.state, pkg.health)"
|
||||
>
|
||||
<svg
|
||||
v-if="isTransitioning"
|
||||
class="animate-spin h-3 w-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span v-if="pkg.state === 'running' && pkg.health === 'unhealthy'" class="w-1.5 h-1.5 rounded-full bg-orange-400 animate-pulse"></span>
|
||||
{{ getStatusLabel(pkg.state, pkg.health) }}
|
||||
</span>
|
||||
<span class="text-xs text-white/50">
|
||||
v{{ pkg.manifest.version }}
|
||||
</span>
|
||||
v-if="tier && tier !== 'optional'"
|
||||
class="tier-badge"
|
||||
:class="tier === 'core' ? 'tier-badge-core' : 'tier-badge-recommended'"
|
||||
>{{ tier }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-white/50">{{ version ? `v${version}` : '' }}</p>
|
||||
<p v-if="author" class="text-xs text-white/40 mt-0.5">{{ author }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-white/70 text-sm mt-3 mb-3 line-clamp-2">
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium"
|
||||
:class="getStatusClass(pkg.state, pkg.health)"
|
||||
>
|
||||
<svg
|
||||
v-if="isTransitioning"
|
||||
class="animate-spin h-3 w-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span v-if="pkg.state === 'running' && pkg.health === 'unhealthy'" class="w-1.5 h-1.5 rounded-full bg-orange-400 animate-pulse"></span>
|
||||
{{ getStatusLabel(pkg.state, pkg.health) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div v-if="!isUninstalling" class="mt-4 flex gap-2">
|
||||
<button
|
||||
@@ -145,9 +153,13 @@ import {
|
||||
isWebOnlyApp, opensInTab, canLaunch,
|
||||
getStatusClass, getStatusLabel, handleImageError,
|
||||
} from './appsConfig'
|
||||
import { getCuratedAppList } from '../discover/curatedApps'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Build a lookup map for enriching sparse backend data during install
|
||||
const curatedMap = new Map(getCuratedAppList().map(a => [a.id, a]))
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
pkg: PackageDataEntry
|
||||
@@ -168,6 +180,35 @@ defineEmits<{
|
||||
|
||||
const isWebOnly = computed(() => isWebOnlyApp(props.id))
|
||||
|
||||
// Enrich from marketplace when backend data is sparse (e.g. during install)
|
||||
const curated = computed(() => curatedMap.get(props.id))
|
||||
const title = computed(() => {
|
||||
const t = props.pkg.manifest?.title
|
||||
return (t && t !== props.id) ? t : (curated.value?.title || t || props.id)
|
||||
})
|
||||
const description = computed(() => {
|
||||
const d = props.pkg.manifest?.description?.short
|
||||
return (d && d !== 'Installing...') ? d : (curated.value?.description || d || '')
|
||||
})
|
||||
const icon = computed(() => {
|
||||
const i = props.pkg['static-files']?.icon
|
||||
return i || curated.value?.icon || `/assets/img/app-icons/${props.id}.png`
|
||||
})
|
||||
const version = computed(() => {
|
||||
const v = props.pkg.manifest?.version
|
||||
return v || curated.value?.version || ''
|
||||
})
|
||||
const author = computed(() => props.pkg.manifest?.author || curated.value?.author || '')
|
||||
const tier = computed(() => {
|
||||
const t = props.pkg.manifest?.tier
|
||||
if (t && t !== '') return t
|
||||
const core = ['bitcoin-knots', 'bitcoin', 'lnd', 'mempool', 'btcpay-server', 'dwn', 'filebrowser']
|
||||
const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'portainer']
|
||||
if (core.includes(props.id)) return 'core'
|
||||
if (recommended.includes(props.id)) return 'recommended'
|
||||
return 'optional'
|
||||
})
|
||||
|
||||
const isTransitioning = computed(() => {
|
||||
const s = props.pkg.state
|
||||
const h = props.pkg.health
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { MarketplaceApp } from './types'
|
||||
export function getCuratedAppList(): MarketplaceApp[] {
|
||||
return [
|
||||
{ id: 'bitcoin-knots', title: 'Bitcoin Knots', version: '28.1.0', description: 'Run a full Bitcoin node. Validate and relay blocks and transactions on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-knots.webp', author: 'Bitcoin Knots', dockerImage: 'docker.io/bitcoinknots/bitcoin:v28.1', repoUrl: 'https://github.com/bitcoinknots/bitcoin' },
|
||||
{ id: 'btcpay-server', title: 'BTCPay Server', version: '1.13.5', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server Foundation', dockerImage: 'docker.io/btcpayserver/btcpayserver:1.13.5', repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
||||
{ id: 'btcpay-server', title: 'BTCPay Server', version: '1.13.7', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server Foundation', dockerImage: 'docker.io/btcpayserver/btcpayserver:1.13.7', repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
||||
{ id: 'lnd', title: 'LND', version: '0.17.4', description: 'Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.', icon: '/assets/img/app-icons/lnd.svg', author: 'Lightning Labs', dockerImage: 'docker.io/lightninglabs/lnd:v0.17.4-beta', repoUrl: 'https://github.com/lightningnetwork/lnd' },
|
||||
{ id: 'thunderhub', title: 'ThunderHub', version: '0.13.31', description: 'Lightning node management UI. Manage channels, payments, routing fees, and monitor your Lightning node.', icon: '/assets/img/app-icons/thunderhub.svg', author: 'Anthony Potdevin', dockerImage: 'docker.io/apotdevin/thunderhub:v0.13.31', repoUrl: 'https://github.com/apotdevin/thunderhub' },
|
||||
{ id: 'mempool', title: 'Mempool Explorer', version: '2.5.0', description: 'Self-hosted Bitcoin blockchain and mempool visualizer. Monitor transactions without revealing your addresses to third parties.', icon: '/assets/img/app-icons/mempool.webp', author: 'Mempool', dockerImage: 'docker.io/mempool/frontend:v2.5.0', repoUrl: 'https://github.com/mempool/mempool' },
|
||||
|
||||
@@ -141,11 +141,11 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
{
|
||||
id: 'btcpay-server',
|
||||
title: 'BTCPay Server',
|
||||
version: '1.13.5',
|
||||
version: '1.13.7',
|
||||
description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.',
|
||||
icon: '/assets/img/app-icons/btcpay-server.png',
|
||||
author: 'BTCPay Server Foundation',
|
||||
dockerImage: `${REGISTRY}/btcpayserver:1.13.5`,
|
||||
dockerImage: `${REGISTRY}/btcpayserver:1.13.7`,
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/btcpayserver/btcpayserver'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user