feat: LUKS2 encryption, boot sequence fixes, onboarding auth, CI/CD
- LUKS2 full-partition encryption for /var/lib/archipelago/ (TASK-42) 4-partition layout: BIOS + EFI + root (30GB) + encrypted data AES-256-XTS with AES-NI detection, ChaCha20 fallback for ARM Auto-unlock via crypttab + random key file - Fix EFI boot errors: remove shim-signed, clean shim artifacts - Fix first-boot sequence: always show boot animation before onboarding - Fix stale localStorage causing login instead of onboarding (BUG-47) - Add auth.setup + auth.isSetup RPC handlers for password on clean install - Add onboarding methods to UNAUTHENTICATED_METHODS (DID sign 403 fix) - FileBrowser bundled in unbundled ISO, fix auto-login Secure cookie (BUG-46) - Kiosk mode: xorg/chromium in rootfs, toggle script, MOTD instructions - Add Gitea Actions CI/CD workflow for automatic ISO builds Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5c15c52113
commit
08bb2c80d4
47
.gitea/workflows/build-iso.yml
Normal file
47
.gitea/workflows/build-iso.yml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
name: Build Archipelago ISO
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-iso:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
run: |
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build backend (release)
|
||||||
|
run: cargo build --release --manifest-path core/Cargo.toml
|
||||||
|
|
||||||
|
- name: Install backend binary
|
||||||
|
run: |
|
||||||
|
sudo cp core/target/release/archipelago /usr/local/bin/archipelago
|
||||||
|
sudo chmod +x /usr/local/bin/archipelago
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: |
|
||||||
|
cd neode-ui
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Build ISO
|
||||||
|
run: |
|
||||||
|
cd image-recipe
|
||||||
|
sudo DEV_SERVER=localhost BUILD_FROM_SOURCE=0 ./build-auto-installer-iso.sh
|
||||||
|
env:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
|
||||||
|
- name: Upload ISO artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: archipelago-iso
|
||||||
|
path: image-recipe/results/*.iso
|
||||||
|
retention-days: 30
|
||||||
@ -66,6 +66,35 @@ impl RpcHandler {
|
|||||||
Ok(serde_json::json!({ "success": true, "session_rotated": true }))
|
Ok(serde_json::json!({ "success": true, "session_rotated": true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn handle_auth_is_setup(&self) -> Result<serde_json::Value> {
|
||||||
|
let is_setup = self.auth_manager.is_setup().await?;
|
||||||
|
Ok(serde_json::json!(is_setup))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn handle_auth_setup(
|
||||||
|
&self,
|
||||||
|
params: Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
// Prevent re-setup if already set up
|
||||||
|
let is_setup = self.auth_manager.is_setup().await?;
|
||||||
|
if is_setup {
|
||||||
|
return Err(anyhow::anyhow!("Already set up. Use auth.changePassword to change."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||||
|
let password = params
|
||||||
|
.get("password")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Missing password"))?;
|
||||||
|
|
||||||
|
if password.len() < 8 {
|
||||||
|
return Err(anyhow::anyhow!("Password must be at least 8 characters"));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.auth_manager.setup_user(password).await?;
|
||||||
|
Ok(serde_json::json!(true))
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_auth_onboarding_complete(&self) -> Result<serde_json::Value> {
|
pub(super) async fn handle_auth_onboarding_complete(&self) -> Result<serde_json::Value> {
|
||||||
self.auth_manager.complete_onboarding().await?;
|
self.auth_manager.complete_onboarding().await?;
|
||||||
Ok(serde_json::json!(true))
|
Ok(serde_json::json!(true))
|
||||||
|
|||||||
@ -16,6 +16,8 @@ impl RpcHandler {
|
|||||||
"auth.login" => self.handle_auth_login(params).await,
|
"auth.login" => self.handle_auth_login(params).await,
|
||||||
"auth.logout" => self.handle_auth_logout().await,
|
"auth.logout" => self.handle_auth_logout().await,
|
||||||
"auth.changePassword" => self.handle_auth_change_password(params, session_token).await,
|
"auth.changePassword" => self.handle_auth_change_password(params, session_token).await,
|
||||||
|
"auth.isSetup" => self.handle_auth_is_setup().await,
|
||||||
|
"auth.setup" => self.handle_auth_setup(params).await,
|
||||||
"auth.onboardingComplete" => self.handle_auth_onboarding_complete().await,
|
"auth.onboardingComplete" => self.handle_auth_onboarding_complete().await,
|
||||||
"auth.isOnboardingComplete" => self.handle_auth_is_onboarding_complete().await,
|
"auth.isOnboardingComplete" => self.handle_auth_is_onboarding_complete().await,
|
||||||
"auth.resetOnboarding" => self.handle_auth_reset_onboarding(params).await,
|
"auth.resetOnboarding" => self.handle_auth_reset_onboarding(params).await,
|
||||||
|
|||||||
@ -8,7 +8,16 @@ pub(super) const UNAUTHENTICATED_METHODS: &[&str] = &[
|
|||||||
"auth.login.backup",
|
"auth.login.backup",
|
||||||
"auth.isOnboardingComplete",
|
"auth.isOnboardingComplete",
|
||||||
"auth.isSetup",
|
"auth.isSetup",
|
||||||
|
"auth.setup",
|
||||||
|
"auth.onboardingComplete",
|
||||||
"health",
|
"health",
|
||||||
|
// Onboarding flow (before user has a session — DID creation, signing, backup)
|
||||||
|
"node.did",
|
||||||
|
"node.signChallenge",
|
||||||
|
"node.nostr-pubkey",
|
||||||
|
"node.createBackup",
|
||||||
|
"identity.verify",
|
||||||
|
"identity.resolve-did",
|
||||||
// Onboarding restore (before user account exists)
|
// Onboarding restore (before user account exists)
|
||||||
"backup.restore-identity",
|
"backup.restore-identity",
|
||||||
// Inter-node RPC: called by federated peers over Tor, no session cookies
|
// Inter-node RPC: called by federated peers over Tor, no session cookies
|
||||||
|
|||||||
@ -17,9 +17,12 @@
|
|||||||
| **TASK-10** | **ISO build verification + multi-hardware test** | **P1** | PLANNED | - |
|
| **TASK-10** | **ISO build verification + multi-hardware test** | **P1** | PLANNED | - |
|
||||||
| **TASK-12** | **Beta telemetry — reporter + toggle + collector POST** | **P1** | IN PROGRESS | - |
|
| **TASK-12** | **Beta telemetry — reporter + toggle + collector POST** | **P1** | IN PROGRESS | - |
|
||||||
| **TASK-39** | **Finish .198 rootless container migration** | **P1** | PLANNED | TASK-11 |
|
| **TASK-39** | **Finish .198 rootless container migration** | **P1** | PLANNED | TASK-11 |
|
||||||
| **TASK-42** | **LUKS2 full-partition encryption for /var/lib/archipelago/** | **P1** | PLANNED | TASK-10 |
|
| **TASK-42** | **LUKS2 full-partition encryption for /var/lib/archipelago/** | **P1** | IN PROGRESS | - |
|
||||||
| **BUG-44** | **App iframe shows blank/broken when container is starting or crashed** | **P2** | PLANNED | - |
|
| **BUG-44** | **App iframe shows blank/broken when container is starting or crashed** | **P2** | PLANNED | - |
|
||||||
| **TASK-45** | **Deploy script: auto-chown data dirs after rootful→rootless migration** | **P2** | PLANNED | - |
|
| **TASK-45** | **Deploy script: auto-chown data dirs after rootful→rootless migration** | **P2** | PLANNED | - |
|
||||||
|
| **BUG-46** | **FileBrowser missing in unbundled ISO + Cloud auto-login broken** | **P1** | IN PROGRESS | - |
|
||||||
|
| **BUG-47** | **Onboarding: DID sign 403 + blob HTTPS + no password setup** | **P1** | IN PROGRESS | - |
|
||||||
|
| **FEATURE-48** | **Meshtastic support for mesh (plug and play)** | **P1** | PLANNED | - |
|
||||||
|
|
||||||
### Phase 2: User Testing (controlled, real hardware)
|
### Phase 2: User Testing (controlled, real hardware)
|
||||||
|
|
||||||
@ -44,6 +47,7 @@
|
|||||||
| **FEATURE-6** | **Watch-only wallet architecture** | **P1** | DEFERRED | - |
|
| **FEATURE-6** | **Watch-only wallet architecture** | **P1** | DEFERRED | - |
|
||||||
| **TASK-7** | **Mesh Bitcoin security hardening** | **P1** | DEFERRED | FEATURE-6 |
|
| **TASK-7** | **Mesh Bitcoin security hardening** | **P1** | DEFERRED | FEATURE-6 |
|
||||||
| **FEATURE-43** | **P2P encrypted voice/video calling (WebRTC over federation)** | **P1** | DEFERRED | - |
|
| **FEATURE-43** | **P2P encrypted voice/video calling (WebRTC over federation)** | **P1** | DEFERRED | - |
|
||||||
|
| **FEATURE-48** | **Meshtastic support for mesh (plug and play)** | **P1** | PLANNED | - |
|
||||||
|
|
||||||
## Active Work
|
## Active Work
|
||||||
|
|
||||||
@ -109,9 +113,9 @@ Tag every significant alpha version with git tags for easy rollback. Each tag sh
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### TASK-42: LUKS2 full-partition encryption for /var/lib/archipelago/ (PLANNED)
|
### TASK-42: LUKS2 full-partition encryption for /var/lib/archipelago/ (IN PROGRESS)
|
||||||
**Priority**: P1 — High
|
**Priority**: P1 — High
|
||||||
**Status**: PLANNED (2026-03-19)
|
**Status**: IN PROGRESS (2026-03-26)
|
||||||
|
|
||||||
Encrypt all Archipelago app data at rest using LUKS2 full-partition encryption. Protects Bitcoin wallet data, LND macaroons, FileBrowser files, Vaultwarden vault, secrets, and everything else from physical disk seizure. Seamless UX — user never interacts with encryption directly.
|
Encrypt all Archipelago app data at rest using LUKS2 full-partition encryption. Protects Bitcoin wallet data, LND macaroons, FileBrowser files, Vaultwarden vault, secrets, and everything else from physical disk seizure. Seamless UX — user never interacts with encryption directly.
|
||||||
|
|
||||||
@ -129,15 +133,15 @@ Encrypt all Archipelago app data at rest using LUKS2 full-partition encryption.
|
|||||||
- Forgot password = cannot decrypt (correct sovereign behavior)
|
- Forgot password = cannot decrypt (correct sovereign behavior)
|
||||||
|
|
||||||
**Tasks**:
|
**Tasks**:
|
||||||
- [ ] ISO installer: create LUKS2 partition, format + mount at `/var/lib/archipelago/`
|
- [x] ISO installer: create LUKS2 partition, format + mount at `/var/lib/archipelago/`
|
||||||
- [ ] First-boot: derive LUKS key from setup password via Argon2id + hardware salt
|
- [ ] First-boot: derive LUKS key from setup password via Argon2id + hardware salt
|
||||||
- [ ] Store key file at `/root/.luks-archipelago.key` with 600 perms
|
- [x] Store key file at `/root/.luks-archipelago.key` with 600 perms
|
||||||
- [ ] Configure `/etc/crypttab` for auto-unlock at boot
|
- [x] Configure `/etc/crypttab` for auto-unlock at boot
|
||||||
- [ ] Settings password change: re-derive LUKS key, add new keyslot, remove old
|
- [ ] Settings password change: re-derive LUKS key, add new keyslot, remove old
|
||||||
- [ ] Detect AES-NI availability, fall back to ChaCha20 on ARM without it
|
- [x] Detect AES-NI availability, fall back to ChaCha20 on ARM without it
|
||||||
- [ ] Test: fresh install, reboot survives, power-cycle survives, password change works
|
- [ ] Test: fresh install, reboot survives, power-cycle survives, password change works
|
||||||
- [ ] Test: disk removed from machine is unreadable
|
- [ ] Test: disk removed from machine is unreadable
|
||||||
- [ ] Update `BUILD-GUIDE.md` and `image-recipe/build-auto-installer-iso.sh`
|
- [x] Update `image-recipe/build-auto-installer-iso.sh`
|
||||||
|
|
||||||
**Key files**:
|
**Key files**:
|
||||||
- `image-recipe/build-auto-installer-iso.sh` — partition creation
|
- `image-recipe/build-auto-installer-iso.sh` — partition creation
|
||||||
@ -176,6 +180,59 @@ Also fix:
|
|||||||
- `scripts/deploy-tailscale.sh` — Step 14 (UID mapping) and Step 22 (container creation)
|
- `scripts/deploy-tailscale.sh` — Step 14 (UID mapping) and Step 22 (container creation)
|
||||||
- `scripts/first-boot-containers.sh` — container creation reference
|
- `scripts/first-boot-containers.sh` — container creation reference
|
||||||
|
|
||||||
|
### BUG-46: FileBrowser missing in unbundled ISO + Cloud auto-login broken (IN PROGRESS)
|
||||||
|
**Priority**: P1 — High
|
||||||
|
**Status**: IN PROGRESS (2026-03-26)
|
||||||
|
|
||||||
|
Two issues with the Cloud feature on fresh installs:
|
||||||
|
|
||||||
|
1. **FileBrowser not prepackaged in unbundled ISO** — The unbundled ISO variant doesn't include the FileBrowser container image, so Cloud doesn't work out of the box. FileBrowser is a core dependency (not an optional app) since it powers the Cloud file manager. Must be bundled even in the unbundled variant.
|
||||||
|
|
||||||
|
2. **FileBrowser auto-login not working** — The auto-login flow (so users don't need to enter separate FileBrowser credentials) appears broken. Need to investigate whether the auth proxy/token injection is functioning correctly on fresh installs.
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [x] Add FileBrowser image to unbundled ISO build (core dependency, always bundled)
|
||||||
|
- [x] Create minimal first-boot script for unbundled mode (FileBrowser only)
|
||||||
|
- [x] Fix auto-login: `Secure` cookie flag silently fails on HTTP — made conditional
|
||||||
|
- [x] Changed `SameSite=Strict` to `SameSite=Lax` for better navigation compatibility
|
||||||
|
- [ ] Test Cloud feature end-to-end on a fresh install (both bundled and unbundled)
|
||||||
|
|
||||||
|
**Key files**:
|
||||||
|
- `image-recipe/build-auto-installer-iso.sh` — UNBUNDLED container image list
|
||||||
|
- `scripts/first-boot-containers.sh` — FileBrowser container creation
|
||||||
|
- `image-recipe/configs/nginx-archipelago.conf` — FileBrowser proxy config
|
||||||
|
- `neode-ui/src/views/Cloud.vue` — Cloud UI / auto-login logic
|
||||||
|
|
||||||
|
### BUG-47: Onboarding: DID sign 403 + blob HTTPS + no password setup (IN PROGRESS)
|
||||||
|
**Priority**: P1 — High
|
||||||
|
**Status**: IN PROGRESS (2026-03-26)
|
||||||
|
|
||||||
|
Three onboarding issues on clean install:
|
||||||
|
|
||||||
|
1. **Sign DID returns 403 Forbidden** — The DID verification/signing step during onboarding fails with a 403 response from the backend.
|
||||||
|
2. **Blob URL HTTPS warning** — Browser complains about blob URL loaded over insecure connection (`blob:http://...` should be served over HTTPS). Likely related to the backup download on HTTP connections.
|
||||||
|
3. **No password setup on clean install** — Users cannot set a password during onboarding. The setup password flow is missing or broken.
|
||||||
|
|
||||||
|
**Root causes found**:
|
||||||
|
- `node.did`, `node.signChallenge`, `node.nostr-pubkey`, `node.createBackup`, `identity.verify` were NOT in `UNAUTHENTICATED_METHODS` — onboarding has no session, so they all returned 403
|
||||||
|
- `auth.setup` and `auth.isSetup` RPC methods were missing from the dispatcher — the frontend called them but no handler existed
|
||||||
|
- Blob HTTPS warning is a browser security feature on HTTP connections (not a code bug)
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
- [x] Add onboarding methods to UNAUTHENTICATED_METHODS in middleware.rs
|
||||||
|
- [x] Add `auth.setup` RPC handler (creates user with password, prevents re-setup)
|
||||||
|
- [x] Add `auth.isSetup` RPC handler (checks if user.json exists)
|
||||||
|
- [x] Rust compiles clean
|
||||||
|
- [ ] Blob URL HTTPS warning — known browser limitation on HTTP, no code fix needed
|
||||||
|
- [ ] Test full onboarding flow end-to-end on fresh ISO
|
||||||
|
|
||||||
|
**Key files**:
|
||||||
|
- `neode-ui/src/views/OnboardingVerify.vue` — DID signing step
|
||||||
|
- `neode-ui/src/views/OnboardingBackup.vue` — Backup download (blob URL)
|
||||||
|
- `neode-ui/src/views/OnboardingIntro.vue` — Password setup entry point
|
||||||
|
- `core/archipelago/src/api/rpc/auth.rs` — Auth RPC endpoints
|
||||||
|
- `core/archipelago/src/api/rpc/middleware.rs` — Request auth middleware
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Post-Beta (FROZEN)
|
## Post-Beta (FROZEN)
|
||||||
|
|||||||
@ -214,7 +214,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
${LINUX_IMAGE_PKG} \
|
${LINUX_IMAGE_PKG} \
|
||||||
${GRUB_EFI_PKG} \
|
${GRUB_EFI_PKG} \
|
||||||
${GRUB_EFI_SIGNED_PKG} \
|
${GRUB_EFI_SIGNED_PKG} \
|
||||||
${GRUB_PC_PKG} shim-signed \
|
${GRUB_PC_PKG} \
|
||||||
systemd \
|
systemd \
|
||||||
systemd-sysv \
|
systemd-sysv \
|
||||||
dbus \
|
dbus \
|
||||||
@ -235,11 +235,15 @@ RUN apt-get update && apt-get install -y \
|
|||||||
locales \
|
locales \
|
||||||
console-setup \
|
console-setup \
|
||||||
keyboard-configuration \
|
keyboard-configuration \
|
||||||
|
cryptsetup \
|
||||||
firmware-realtek \
|
firmware-realtek \
|
||||||
firmware-iwlwifi \
|
firmware-iwlwifi \
|
||||||
firmware-misc-nonfree \
|
firmware-misc-nonfree \
|
||||||
intel-microcode \
|
intel-microcode \
|
||||||
amd64-microcode \
|
amd64-microcode \
|
||||||
|
xorg \
|
||||||
|
chromium \
|
||||||
|
unclutter \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@ -611,10 +615,25 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ "$UNBUNDLED" = "1" ]; then
|
if [ "$UNBUNDLED" = "1" ]; then
|
||||||
echo "📦 Step 3b: SKIPPING container image bundling (UNBUNDLED mode)"
|
echo "📦 Step 3b: Bundling core containers only (UNBUNDLED mode)"
|
||||||
echo " Apps will be downloaded on-demand from the Marketplace after install."
|
echo " Optional apps will be downloaded on-demand from the Marketplace after install."
|
||||||
IMAGES_DIR="$ARCH_DIR/container-images"
|
IMAGES_DIR="$ARCH_DIR/container-images"
|
||||||
mkdir -p "$IMAGES_DIR"
|
mkdir -p "$IMAGES_DIR"
|
||||||
|
# FileBrowser is a core dependency (powers the Cloud file manager) — always bundle it
|
||||||
|
CORE_IMAGE="${FILEBROWSER_IMAGE:-docker.io/filebrowser/filebrowser:v2}"
|
||||||
|
CORE_FILE="filebrowser.tar"
|
||||||
|
if [ -f "$IMAGES_DIR/$CORE_FILE" ]; then
|
||||||
|
echo " ✅ Using cached: $CORE_FILE"
|
||||||
|
else
|
||||||
|
echo " Pulling $CORE_IMAGE ($CONTAINER_PLATFORM)..."
|
||||||
|
if $CONTAINER_CMD pull --platform $CONTAINER_PLATFORM "$CORE_IMAGE"; then
|
||||||
|
$CONTAINER_CMD save "$CORE_IMAGE" -o "$IMAGES_DIR/$CORE_FILE" 2>/dev/null && \
|
||||||
|
echo " ✅ Saved core: $CORE_FILE ($(du -h "$IMAGES_DIR/$CORE_FILE" | cut -f1))" || \
|
||||||
|
echo " ⚠️ Failed to save $CORE_IMAGE"
|
||||||
|
else
|
||||||
|
echo " ⚠️ Failed to pull $CORE_IMAGE — Cloud will not work until installed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "📦 Step 3b: Bundling container images for offline use..."
|
echo "📦 Step 3b: Bundling container images for offline use..."
|
||||||
|
|
||||||
@ -877,9 +896,58 @@ cp "$WORK_DIR/setup-tor.sh" "$ARCH_DIR/scripts/"
|
|||||||
cp "$WORK_DIR/archipelago-setup-tor.service" "$ARCH_DIR/scripts/"
|
cp "$WORK_DIR/archipelago-setup-tor.service" "$ARCH_DIR/scripts/"
|
||||||
|
|
||||||
# First-boot: create core containers (bitcoin, mempool, btcpay, lnd, fedimint, homeassistant)
|
# First-boot: create core containers (bitcoin, mempool, btcpay, lnd, fedimint, homeassistant)
|
||||||
# Skip for unbundled builds — no images pre-loaded, users install from Marketplace
|
# Unbundled builds only create FileBrowser (core dependency for Cloud)
|
||||||
if [ "$UNBUNDLED" = "1" ]; then
|
if [ "$UNBUNDLED" = "1" ]; then
|
||||||
echo " Skipping first-boot containers (UNBUNDLED: apps installed from Marketplace)"
|
echo " Creating minimal first-boot service (UNBUNDLED: FileBrowser only)..."
|
||||||
|
# Create a minimal first-boot script that only starts FileBrowser
|
||||||
|
cat > "$WORK_DIR/first-boot-containers-unbundled.sh" <<'FBUNBUNDLED'
|
||||||
|
#!/bin/bash
|
||||||
|
# Minimal first-boot: create FileBrowser container only (unbundled ISO)
|
||||||
|
set -e
|
||||||
|
DOCKER="podman"
|
||||||
|
LOG="/var/log/archipelago-first-boot.log"
|
||||||
|
echo "[$(date)] Starting minimal first-boot (unbundled)..." >> "$LOG"
|
||||||
|
|
||||||
|
# Create Cloud storage directories
|
||||||
|
mkdir -p /var/lib/archipelago/filebrowser
|
||||||
|
mkdir -p /var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads}
|
||||||
|
chown -R 1000:1000 /var/lib/archipelago/filebrowser
|
||||||
|
chown -R 1000:1000 /var/lib/archipelago/data
|
||||||
|
|
||||||
|
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then
|
||||||
|
echo "[$(date)] Creating FileBrowser container..." >> "$LOG"
|
||||||
|
$DOCKER run -d --name filebrowser --restart unless-stopped \
|
||||||
|
--health-cmd="curl -sf http://localhost:80/ || exit 1" \
|
||||||
|
--health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||||
|
--memory=256m \
|
||||||
|
-p 8083:80 \
|
||||||
|
-v /var/lib/archipelago/filebrowser:/srv \
|
||||||
|
docker.io/filebrowser/filebrowser:v2 2>>"$LOG" && \
|
||||||
|
echo "[$(date)] FileBrowser created successfully" >> "$LOG" || \
|
||||||
|
echo "[$(date)] WARNING: FileBrowser creation failed" >> "$LOG"
|
||||||
|
fi
|
||||||
|
echo "[$(date)] Minimal first-boot complete" >> "$LOG"
|
||||||
|
FBUNBUNDLED
|
||||||
|
chmod +x "$WORK_DIR/first-boot-containers-unbundled.sh"
|
||||||
|
cp "$WORK_DIR/first-boot-containers-unbundled.sh" "$ARCH_DIR/scripts/first-boot-containers.sh"
|
||||||
|
|
||||||
|
cat > "$WORK_DIR/archipelago-first-boot-containers.service" <<'FBCSERVICE'
|
||||||
|
[Unit]
|
||||||
|
Description=Create core Archipelago containers on first boot
|
||||||
|
After=archipelago-setup-tor.service network-online.target podman.service
|
||||||
|
ConditionPathExists=/opt/archipelago/scripts/first-boot-containers.sh
|
||||||
|
ConditionPathExists=!/var/lib/archipelago/.first-boot-containers-done
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/opt/archipelago/scripts/first-boot-containers.sh
|
||||||
|
ExecStartPost=/usr/bin/touch /var/lib/archipelago/.first-boot-containers-done
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
FBCSERVICE
|
||||||
|
cp "$WORK_DIR/archipelago-first-boot-containers.service" "$ARCH_DIR/scripts/"
|
||||||
else
|
else
|
||||||
echo " Creating first-boot container creation service..."
|
echo " Creating first-boot container creation service..."
|
||||||
# Copy shared script library
|
# Copy shared script library
|
||||||
@ -1112,8 +1180,8 @@ echo ""
|
|||||||
umount ${TARGET_DISK}* 2>/dev/null || true
|
umount ${TARGET_DISK}* 2>/dev/null || true
|
||||||
umount ${TARGET_DISK}p* 2>/dev/null || true
|
umount ${TARGET_DISK}p* 2>/dev/null || true
|
||||||
|
|
||||||
# Create partition table — dual BIOS+UEFI boot support
|
# Create partition table — dual BIOS+UEFI boot + LUKS2 encrypted data
|
||||||
echo " [1/6] Creating partitions..."
|
echo " [1/7] Creating partitions..."
|
||||||
parted -s "$TARGET_DISK" mklabel gpt
|
parted -s "$TARGET_DISK" mklabel gpt
|
||||||
# Partition 1: 1MB BIOS boot partition (for legacy BIOS GRUB on GPT disks)
|
# Partition 1: 1MB BIOS boot partition (for legacy BIOS GRUB on GPT disks)
|
||||||
parted -s "$TARGET_DISK" mkpart bios_boot 1MiB 2MiB
|
parted -s "$TARGET_DISK" mkpart bios_boot 1MiB 2MiB
|
||||||
@ -1121,8 +1189,10 @@ parted -s "$TARGET_DISK" set 1 bios_grub on
|
|||||||
# Partition 2: 512MB EFI System Partition (for UEFI boot)
|
# Partition 2: 512MB EFI System Partition (for UEFI boot)
|
||||||
parted -s "$TARGET_DISK" mkpart efi fat32 2MiB 514MiB
|
parted -s "$TARGET_DISK" mkpart efi fat32 2MiB 514MiB
|
||||||
parted -s "$TARGET_DISK" set 2 esp on
|
parted -s "$TARGET_DISK" set 2 esp on
|
||||||
# Partition 3: Root filesystem (remaining space)
|
# Partition 3: Root filesystem (30GB — system, packages, container runtime)
|
||||||
parted -s "$TARGET_DISK" mkpart root ext4 514MiB 100%
|
parted -s "$TARGET_DISK" mkpart root ext4 514MiB 30GiB
|
||||||
|
# Partition 4: Encrypted data (LUKS2 — Bitcoin data, secrets, app volumes)
|
||||||
|
parted -s "$TARGET_DISK" mkpart data 30GiB 100%
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
@ -1131,36 +1201,91 @@ if [[ "$TARGET_DISK" == *nvme* ]]; then
|
|||||||
BIOS_PART="${TARGET_DISK}p1"
|
BIOS_PART="${TARGET_DISK}p1"
|
||||||
EFI_PART="${TARGET_DISK}p2"
|
EFI_PART="${TARGET_DISK}p2"
|
||||||
ROOT_PART="${TARGET_DISK}p3"
|
ROOT_PART="${TARGET_DISK}p3"
|
||||||
|
DATA_PART="${TARGET_DISK}p4"
|
||||||
else
|
else
|
||||||
BIOS_PART="${TARGET_DISK}1"
|
BIOS_PART="${TARGET_DISK}1"
|
||||||
EFI_PART="${TARGET_DISK}2"
|
EFI_PART="${TARGET_DISK}2"
|
||||||
ROOT_PART="${TARGET_DISK}3"
|
ROOT_PART="${TARGET_DISK}3"
|
||||||
|
DATA_PART="${TARGET_DISK}4"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Format partitions
|
# Format partitions
|
||||||
echo " [2/6] Formatting partitions..."
|
echo " [2/7] Formatting partitions..."
|
||||||
# Zero out the BIOS boot partition to prevent FAT-fs read errors during boot
|
# Zero out the BIOS boot partition to prevent FAT-fs read errors during boot
|
||||||
dd if=/dev/zero of="$BIOS_PART" bs=1M count=1 2>/dev/null || true
|
dd if=/dev/zero of="$BIOS_PART" bs=1M count=1 2>/dev/null || true
|
||||||
mkfs.vfat -F32 -n EFI "$EFI_PART"
|
mkfs.vfat -F32 -n EFI "$EFI_PART"
|
||||||
mkfs.ext4 -F -L archipelago "$ROOT_PART"
|
mkfs.ext4 -F -L archipelago "$ROOT_PART"
|
||||||
|
|
||||||
# Mount
|
# Mount root + extract rootfs (need cryptsetup from rootfs for LUKS)
|
||||||
echo " [3/6] Mounting filesystems..."
|
echo " [3/7] Mounting filesystems..."
|
||||||
mkdir -p /mnt/target
|
mkdir -p /mnt/target
|
||||||
mount "$ROOT_PART" /mnt/target
|
mount "$ROOT_PART" /mnt/target
|
||||||
mkdir -p /mnt/target/boot/efi
|
mkdir -p /mnt/target/boot/efi
|
||||||
mount "$EFI_PART" /mnt/target/boot/efi
|
mount "$EFI_PART" /mnt/target/boot/efi
|
||||||
|
|
||||||
# Extract rootfs
|
echo " [4/7] Installing system (this may take a few minutes)..."
|
||||||
echo " [4/6] Installing system (this may take a few minutes)..."
|
|
||||||
tar -xf "$ROOTFS_TAR" -C /mnt/target
|
tar -xf "$ROOTFS_TAR" -C /mnt/target
|
||||||
|
|
||||||
|
# LUKS2 encryption for data partition
|
||||||
|
echo " [5/7] Encrypting data partition (LUKS2)..."
|
||||||
|
|
||||||
|
# Generate random 4KB key file
|
||||||
|
dd if=/dev/urandom of=/mnt/target/root/.luks-archipelago.key bs=4096 count=1 2>/dev/null
|
||||||
|
chmod 600 /mnt/target/root/.luks-archipelago.key
|
||||||
|
|
||||||
|
# Bind-mount /dev so cryptsetup can access the data partition from chroot
|
||||||
|
mount --bind /dev /mnt/target/dev
|
||||||
|
|
||||||
|
# Detect AES-NI support for cipher selection
|
||||||
|
if grep -q aes /proc/cpuinfo 2>/dev/null; then
|
||||||
|
LUKS_CIPHER="aes-xts-plain64"
|
||||||
|
echo " AES-NI detected — using AES-256-XTS"
|
||||||
|
else
|
||||||
|
LUKS_CIPHER="xchacha20,aes-adiantum-plain64"
|
||||||
|
echo " No AES-NI — using ChaCha20-Adiantum"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Format LUKS2 partition with key file
|
||||||
|
chroot /mnt/target cryptsetup luksFormat --type luks2 \
|
||||||
|
--key-file /root/.luks-archipelago.key \
|
||||||
|
--cipher "$LUKS_CIPHER" --key-size 512 \
|
||||||
|
--pbkdf argon2id --batch-mode \
|
||||||
|
"$DATA_PART"
|
||||||
|
|
||||||
|
# Open the LUKS volume
|
||||||
|
chroot /mnt/target cryptsetup open --type luks2 \
|
||||||
|
--key-file /root/.luks-archipelago.key \
|
||||||
|
"$DATA_PART" archipelago-data
|
||||||
|
|
||||||
|
# Unmount /dev (will be re-mounted later for grub-install)
|
||||||
|
umount /mnt/target/dev
|
||||||
|
|
||||||
|
# Format the inner filesystem
|
||||||
|
mkfs.ext4 -F -L archipelago-data /dev/mapper/archipelago-data
|
||||||
|
|
||||||
|
# Mount encrypted partition
|
||||||
|
mkdir -p /mnt/target/var/lib/archipelago
|
||||||
|
mount /dev/mapper/archipelago-data /mnt/target/var/lib/archipelago
|
||||||
|
|
||||||
|
# Recreate directory structure on encrypted partition
|
||||||
|
mkdir -p /mnt/target/var/lib/archipelago/{data,config,containers,secrets,tor,identities,lnd}
|
||||||
|
mkdir -p /mnt/target/var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads}
|
||||||
|
chown -R 1000:1000 /mnt/target/var/lib/archipelago
|
||||||
|
|
||||||
|
echo " ✅ Data partition encrypted with LUKS2 ($LUKS_CIPHER)"
|
||||||
|
|
||||||
|
# Configure auto-unlock via crypttab (key file on root partition)
|
||||||
|
echo " [6/7] Configuring system..."
|
||||||
|
DATA_UUID=$(blkid -s UUID -o value "$DATA_PART")
|
||||||
|
echo "# LUKS2 encrypted data — auto-unlock with key file" > /mnt/target/etc/crypttab
|
||||||
|
echo "archipelago-data UUID=$DATA_UUID /root/.luks-archipelago.key luks,discard" >> /mnt/target/etc/crypttab
|
||||||
|
|
||||||
# Create fstab
|
# Create fstab
|
||||||
echo " [5/6] Configuring system..."
|
|
||||||
cat > /mnt/target/etc/fstab <<EOF
|
cat > /mnt/target/etc/fstab <<EOF
|
||||||
# Archipelago Bitcoin Node OS
|
# Archipelago Bitcoin Node OS
|
||||||
UUID=$(blkid -s UUID -o value "$ROOT_PART") / ext4 errors=remount-ro 0 1
|
UUID=$(blkid -s UUID -o value "$ROOT_PART") / ext4 errors=remount-ro 0 1
|
||||||
UUID=$(blkid -s UUID -o value "$EFI_PART") /boot/efi vfat umask=0077 0 1
|
UUID=$(blkid -s UUID -o value "$EFI_PART") /boot/efi vfat umask=0077 0 1
|
||||||
|
/dev/mapper/archipelago-data /var/lib/archipelago ext4 defaults 0 2
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Configure hostname
|
# Configure hostname
|
||||||
@ -1325,6 +1450,18 @@ if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then
|
|||||||
echo " 🔑 Password: archipelago (SSH) / password123 (Web UI)"
|
echo " 🔑 Password: archipelago (SSH) / password123 (Web UI)"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
if [ -b /dev/mapper/archipelago-data ]; then
|
||||||
|
echo " 🔒 Storage: LUKS2 encrypted"
|
||||||
|
fi
|
||||||
|
echo " 📺 Display Mode:"
|
||||||
|
if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then
|
||||||
|
echo " Kiosk ACTIVE — fullscreen web UI on display"
|
||||||
|
else
|
||||||
|
echo " Console login (MOTD)"
|
||||||
|
fi
|
||||||
|
echo " Toggle: sudo archipelago-kiosk enable — kiosk on display"
|
||||||
|
echo " sudo archipelago-kiosk disable — back to console"
|
||||||
|
echo ""
|
||||||
fi
|
fi
|
||||||
PROFILE
|
PROFILE
|
||||||
chmod +x /mnt/target/etc/profile.d/archipelago.sh
|
chmod +x /mnt/target/etc/profile.d/archipelago.sh
|
||||||
@ -1422,8 +1559,115 @@ RestartSec=5
|
|||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
CLAUDESVC
|
CLAUDESVC
|
||||||
|
|
||||||
|
# Kiosk mode — X11 + Chromium fullscreen on attached display
|
||||||
|
# Not enabled by default; toggle via: sudo archipelago-kiosk enable/disable
|
||||||
|
cat > /mnt/target/usr/local/bin/archipelago-kiosk-launcher <<'KIOSKLAUNCHER'
|
||||||
|
#!/bin/bash
|
||||||
|
# Start X server in the background
|
||||||
|
/usr/bin/Xorg :0 -nocursor vt1 -nolisten tcp -keeptty &
|
||||||
|
XPID=$!
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if ! kill -0 $XPID 2>/dev/null; then
|
||||||
|
echo 'ERROR: Xorg failed to start'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DISPLAY=:0
|
||||||
|
export HOME=/home/archipelago
|
||||||
|
|
||||||
|
xhost +SI:localuser:archipelago 2>/dev/null
|
||||||
|
xset s off 2>/dev/null
|
||||||
|
xset -dpms 2>/dev/null
|
||||||
|
xset s noblank 2>/dev/null
|
||||||
|
|
||||||
|
unclutter -idle 3 -root &
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago chromium \
|
||||||
|
--kiosk \
|
||||||
|
--app=http://localhost/kiosk \
|
||||||
|
--noerrdialogs \
|
||||||
|
--disable-infobars \
|
||||||
|
--disable-translate \
|
||||||
|
--no-first-run \
|
||||||
|
--check-for-update-interval=31536000 \
|
||||||
|
--disable-features=TranslateUI \
|
||||||
|
--disable-session-crashed-bubble \
|
||||||
|
--disable-save-password-bubble \
|
||||||
|
--disable-suggestions-service \
|
||||||
|
--disable-component-update \
|
||||||
|
--disable-gpu \
|
||||||
|
--user-data-dir=/home/archipelago/.config/chromium-kiosk
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
|
||||||
|
kill $XPID 2>/dev/null
|
||||||
|
KIOSKLAUNCHER
|
||||||
|
chmod +x /mnt/target/usr/local/bin/archipelago-kiosk-launcher
|
||||||
|
|
||||||
|
cat > /mnt/target/etc/systemd/system/archipelago-kiosk.service <<'KIOSKSVC'
|
||||||
|
[Unit]
|
||||||
|
Description=Archipelago Kiosk (X11 + Chromium)
|
||||||
|
After=archipelago.service
|
||||||
|
Wants=archipelago.service
|
||||||
|
ConditionPathExists=/usr/local/bin/archipelago-kiosk-launcher
|
||||||
|
Conflicts=getty@tty1.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStartPre=/bin/bash -c 'for i in $(seq 1 15); do curl -sf http://localhost/health >/dev/null 2>&1 && exit 0; sleep 2; done; exit 0'
|
||||||
|
ExecStart=/usr/local/bin/archipelago-kiosk-launcher
|
||||||
|
TimeoutStartSec=60
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
KIOSKSVC
|
||||||
|
|
||||||
|
# Toggle script: sudo archipelago-kiosk enable|disable|status
|
||||||
|
cat > /mnt/target/usr/local/bin/archipelago-kiosk <<'KIOSKTOGGLE'
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "${1:-status}" in
|
||||||
|
enable)
|
||||||
|
echo "Enabling kiosk mode (X11 + Chromium on display)..."
|
||||||
|
systemctl enable archipelago-kiosk.service
|
||||||
|
systemctl start archipelago-kiosk.service 2>/dev/null || true
|
||||||
|
echo "Kiosk mode ENABLED. Console login (tty1) is now disabled."
|
||||||
|
echo "To access the server, use SSH or the web UI."
|
||||||
|
;;
|
||||||
|
disable)
|
||||||
|
echo "Disabling kiosk mode (restoring console login)..."
|
||||||
|
systemctl stop archipelago-kiosk.service 2>/dev/null || true
|
||||||
|
systemctl disable archipelago-kiosk.service
|
||||||
|
systemctl restart getty@tty1.service 2>/dev/null || true
|
||||||
|
echo "Kiosk mode DISABLED. Console login restored on tty1."
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
if systemctl is-active archipelago-kiosk.service >/dev/null 2>&1; then
|
||||||
|
echo "Kiosk mode: ACTIVE (display showing web UI)"
|
||||||
|
elif systemctl is-enabled archipelago-kiosk.service >/dev/null 2>&1; then
|
||||||
|
echo "Kiosk mode: ENABLED (will start on next boot)"
|
||||||
|
else
|
||||||
|
echo "Kiosk mode: DISABLED (console login on tty1)"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: archipelago-kiosk [enable|disable|status]"
|
||||||
|
echo " enable — Start kiosk (fullscreen web UI on display)"
|
||||||
|
echo " disable — Stop kiosk, restore console login"
|
||||||
|
echo " status — Show current mode"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
KIOSKTOGGLE
|
||||||
|
chmod +x /mnt/target/usr/local/bin/archipelago-kiosk
|
||||||
|
|
||||||
# Install GRUB
|
# Install GRUB
|
||||||
echo " [6/6] Installing bootloader..."
|
echo " [7/7] Installing bootloader..."
|
||||||
mount --bind /dev /mnt/target/dev
|
mount --bind /dev /mnt/target/dev
|
||||||
mount --bind /dev/pts /mnt/target/dev/pts
|
mount --bind /dev/pts /mnt/target/dev/pts
|
||||||
mount --bind /proc /mnt/target/proc
|
mount --bind /proc /mnt/target/proc
|
||||||
@ -1462,10 +1706,10 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# EFI boot: grub-install --removable already placed unsigned GRUB at /EFI/BOOT/BOOTX64.EFI
|
# EFI boot: grub-install --removable places unsigned GRUB at /EFI/BOOT/BOOTX64.EFI
|
||||||
# This works on all machines without Secure Boot. For Secure Boot, users must disable it.
|
# No shim chain — Secure Boot must be disabled. shim-signed was removed from rootfs
|
||||||
# The shim chain was causing "Failed to open \EFI\BOOT\" errors with garbled filenames
|
# because it installs BOOTX64.CSV + shimx64.efi which cause "Failed to open \EFI\BOOT\"
|
||||||
# on machines where Secure Boot is disabled — the shim tries to verify signatures and fails.
|
# errors with garbled filenames on every boot.
|
||||||
echo " Verifying EFI boot files..."
|
echo " Verifying EFI boot files..."
|
||||||
EFI_BOOT_DIR="/mnt/target/boot/efi/EFI/BOOT"
|
EFI_BOOT_DIR="/mnt/target/boot/efi/EFI/BOOT"
|
||||||
if [ "$ARCH" = "x86_64" ]; then
|
if [ "$ARCH" = "x86_64" ]; then
|
||||||
@ -1473,6 +1717,16 @@ if [ "$ARCH" = "x86_64" ]; then
|
|||||||
else
|
else
|
||||||
EFI_BOOT_BINARY="BOOTAA64.EFI"
|
EFI_BOOT_BINARY="BOOTAA64.EFI"
|
||||||
fi
|
fi
|
||||||
|
# Remove any residual shim chain files (from grub-efi-*-signed package hooks)
|
||||||
|
# These cause firmware to try loading garbled vendor paths before falling back
|
||||||
|
for shim_file in shimx64.efi mmx64.efi fbx64.efi BOOTX64.CSV shimaa64.efi mmaa64.efi fbaa64.efi BOOTAA64.CSV; do
|
||||||
|
if [ -f "$EFI_BOOT_DIR/$shim_file" ] && [ "$shim_file" != "$EFI_BOOT_BINARY" ]; then
|
||||||
|
rm -f "$EFI_BOOT_DIR/$shim_file"
|
||||||
|
echo " Removed shim artifact: $shim_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Also remove vendor-specific EFI directory (shim creates /EFI/archipelago/)
|
||||||
|
rm -rf "/mnt/target/boot/efi/EFI/archipelago" 2>/dev/null || true
|
||||||
if [ -f "$EFI_BOOT_DIR/$EFI_BOOT_BINARY" ]; then
|
if [ -f "$EFI_BOOT_DIR/$EFI_BOOT_BINARY" ]; then
|
||||||
echo " ✅ UEFI boot binary present: $EFI_BOOT_DIR/$EFI_BOOT_BINARY"
|
echo " ✅ UEFI boot binary present: $EFI_BOOT_DIR/$EFI_BOOT_BINARY"
|
||||||
ls -la "$EFI_BOOT_DIR/"
|
ls -la "$EFI_BOOT_DIR/"
|
||||||
@ -1532,6 +1786,8 @@ umount /mnt/target/proc 2>/dev/null || true
|
|||||||
umount /mnt/target/dev/pts 2>/dev/null || true
|
umount /mnt/target/dev/pts 2>/dev/null || true
|
||||||
umount /mnt/target/dev 2>/dev/null || true
|
umount /mnt/target/dev 2>/dev/null || true
|
||||||
umount /mnt/target/boot/efi 2>/dev/null || true
|
umount /mnt/target/boot/efi 2>/dev/null || true
|
||||||
|
umount /mnt/target/var/lib/archipelago 2>/dev/null || true
|
||||||
|
cryptsetup close archipelago-data 2>/dev/null || true
|
||||||
umount /mnt/target 2>/dev/null || true
|
umount /mnt/target 2>/dev/null || true
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@ -65,7 +65,9 @@ class FileBrowserClient {
|
|||||||
const token = text.replace(/^"|"$/g, '')
|
const token = text.replace(/^"|"$/g, '')
|
||||||
// Store token as cookie — the only auth mechanism we use
|
// Store token as cookie — the only auth mechanism we use
|
||||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString()
|
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString()
|
||||||
document.cookie = `auth=${token}; path=/app/filebrowser; SameSite=Strict; Secure; expires=${expires}`
|
// 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
|
this._authenticated = true
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -38,6 +38,19 @@ async function quickHealthCheck(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkOnboarded(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await Promise.race([
|
||||||
|
isOnboardingComplete(),
|
||||||
|
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 3000)),
|
||||||
|
])
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
// Backend unreachable — fall back to localStorage only as last resort
|
||||||
|
return localStorage.getItem('neode_onboarding_complete') === '1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function proceedToApp() {
|
async function proceedToApp() {
|
||||||
const devMode = import.meta.env.VITE_DEV_MODE
|
const devMode = import.meta.env.VITE_DEV_MODE
|
||||||
if (devMode === 'setup' || devMode === 'existing') {
|
if (devMode === 'setup' || devMode === 'existing') {
|
||||||
@ -45,23 +58,10 @@ async function proceedToApp() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const localComplete = localStorage.getItem('neode_onboarding_complete') === '1'
|
// Always check backend for authoritative onboarding state
|
||||||
if (localComplete) {
|
// (localStorage can be stale from a previous install on the same IP)
|
||||||
router.replace('/login').catch(() => {})
|
const onboarded = await checkOnboarded()
|
||||||
return
|
router.replace(onboarded ? '/login' : '/onboarding/intro').catch(() => {})
|
||||||
}
|
|
||||||
|
|
||||||
let seenOnboarding = false
|
|
||||||
try {
|
|
||||||
const result = await Promise.race([
|
|
||||||
isOnboardingComplete(),
|
|
||||||
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 3000)),
|
|
||||||
])
|
|
||||||
seenOnboarding = result
|
|
||||||
} catch {
|
|
||||||
seenOnboarding = false
|
|
||||||
}
|
|
||||||
router.replace(seenOnboarding ? '/login' : '/onboarding/intro').catch(() => {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onServerReady() {
|
function onServerReady() {
|
||||||
@ -98,14 +98,23 @@ onMounted(async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Production: quick health check
|
// Production: check server health
|
||||||
const isUp = await quickHealthCheck()
|
const isUp = await quickHealthCheck()
|
||||||
|
|
||||||
if (isUp) {
|
if (isUp) {
|
||||||
proceedToApp()
|
// Server is up — check if onboarding is complete
|
||||||
return
|
const onboarded = await checkOnboarded()
|
||||||
|
if (onboarded) {
|
||||||
|
// Returning user, server is up — go straight to login
|
||||||
|
proceedToApp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// First boot: server is up but onboarding not done yet.
|
||||||
|
// Show boot animation anyway — it lets services fully warm up
|
||||||
|
// (containers, DID resolver, etc.) before onboarding starts.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server not ready — show boot screen
|
// Server not ready OR first boot — show boot screen
|
||||||
showBootScreen.value = true
|
showBootScreen.value = true
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user