diff --git a/.gitignore b/.gitignore index cb6d28b4e..559918a61 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules/ .ruff_cache/ .venv .pnpm-store +.DS_Store diff --git a/README.es.md b/README.es.md index c2f55f366..06dc7abbc 100644 --- a/README.es.md +++ b/README.es.md @@ -81,15 +81,20 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7 Ejecuta SurfSense en tu propia infraestructura para control total de datos y privacidad. -**Requisitos previos:** [Docker](https://docs.docker.com/get-docker/) (con [Docker Compose](https://docs.docker.com/compose/install/)) debe estar instalado y en ejecución. +**Requisitos previos:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) debe estar instalado y en ejecución. -> [!NOTE] -> Usuarios de Windows: instalen [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) primero y ejecuten el siguiente comando en la terminal de Ubuntu. +#### Para usuarios de Linux/MacOS: ```bash curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash ``` +#### Para usuarios de Windows: + +```powershell +irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex +``` + El script de instalación configura [Watchtower](https://github.com/nicholas-fedor/watchtower) automáticamente para actualizaciones diarias. Para omitirlo, agrega la bandera `--no-watchtower`. Para Docker Compose, instalación manual y otras opciones de despliegue, consulta la [documentación](https://www.surfsense.com/docs/). diff --git a/README.hi.md b/README.hi.md index 066e01eb7..22acebb8c 100644 --- a/README.hi.md +++ b/README.hi.md @@ -81,15 +81,20 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7 पूर्ण डेटा नियंत्रण और गोपनीयता के लिए SurfSense को अपने स्वयं के बुनियादी ढांचे पर चलाएं। -**आवश्यकताएँ:** [Docker](https://docs.docker.com/get-docker/) ([Docker Compose](https://docs.docker.com/compose/install/) सहित) इंस्टॉल और चालू होना चाहिए। +**आवश्यकताएँ:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) इंस्टॉल और चालू होना चाहिए। -> [!NOTE] -> Windows उपयोगकर्ता: पहले [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) इंस्टॉल करें और नीचे दिया गया कमांड Ubuntu टर्मिनल में चलाएं। +#### Linux/MacOS उपयोगकर्ताओं के लिए: ```bash curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash ``` +#### Windows उपयोगकर्ताओं के लिए: + +```powershell +irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex +``` + इंस्टॉल स्क्रिप्ट दैनिक ऑटो-अपडेट के लिए स्वचालित रूप से [Watchtower](https://github.com/nicholas-fedor/watchtower) सेटअप करती है। इसे छोड़ने के लिए, `--no-watchtower` फ्लैग जोड़ें। Docker Compose, मैनुअल इंस्टॉलेशन और अन्य डिप्लॉयमेंट विकल्पों के लिए, [डॉक्स](https://www.surfsense.com/docs/) देखें। diff --git a/README.md b/README.md index 7641aa202..0ca61c746 100644 --- a/README.md +++ b/README.md @@ -81,15 +81,20 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7 Run SurfSense on your own infrastructure for full data control and privacy. -**Prerequisites:** [Docker](https://docs.docker.com/get-docker/) (with [Docker Compose](https://docs.docker.com/compose/install/)) must be installed and running. +**Prerequisites:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) must be installed and running. -> [!NOTE] -> Windows users: install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) first and run the command below in the Ubuntu terminal. +#### For Linux/MacOS users: ```bash curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash ``` +#### For Windows users: + +```bash +irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex +``` + The install script sets up [Watchtower](https://github.com/nicholas-fedor/watchtower) automatically for daily auto-updates. To skip it, add the `--no-watchtower` flag. For Docker Compose, manual installation, and other deployment options, see the [docs](https://www.surfsense.com/docs/). diff --git a/README.pt-BR.md b/README.pt-BR.md index d2e45fe5f..e537cb37f 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -81,15 +81,20 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7 Execute o SurfSense na sua própria infraestrutura para controle total de dados e privacidade. -**Pré-requisitos:** [Docker](https://docs.docker.com/get-docker/) (com [Docker Compose](https://docs.docker.com/compose/install/)) deve estar instalado e em execução. +**Pré-requisitos:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) deve estar instalado e em execução. -> [!NOTE] -> Usuários do Windows: instalem o [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) primeiro e executem o comando abaixo no terminal do Ubuntu. +#### Para usuários de Linux/MacOS: ```bash curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash ``` +#### Para usuários do Windows: + +```powershell +irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex +``` + O script de instalação configura o [Watchtower](https://github.com/nicholas-fedor/watchtower) automaticamente para atualizações diárias. Para pular, adicione a flag `--no-watchtower`. Para Docker Compose, instalação manual e outras opções de implantação, consulte a [documentação](https://www.surfsense.com/docs/). diff --git a/README.zh-CN.md b/README.zh-CN.md index 218252388..128a9780a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -81,15 +81,20 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7 在您自己的基础设施上运行 SurfSense,实现完全的数据控制和隐私保护。 -**前置条件:** 需要安装并运行 [Docker](https://docs.docker.com/get-docker/)(含 [Docker Compose](https://docs.docker.com/compose/install/))。 +**前置条件:** 需要安装并运行 [Docker Desktop](https://www.docker.com/products/docker-desktop/)。 -> [!NOTE] -> Windows 用户:请先安装 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install),然后在 Ubuntu 终端中运行以下命令。 +#### Linux/MacOS 用户: ```bash curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash ``` +#### Windows 用户: + +```powershell +irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex +``` + 安装脚本会自动配置 [Watchtower](https://github.com/nicholas-fedor/watchtower) 以实现每日自动更新。如需跳过,请添加 `--no-watchtower` 参数。 如需 Docker Compose、手动安装及其他部署方式,请查看[文档](https://www.surfsense.com/docs/)。 diff --git a/docker/scripts/install.ps1 b/docker/scripts/install.ps1 new file mode 100644 index 000000000..fc9c75a28 --- /dev/null +++ b/docker/scripts/install.ps1 @@ -0,0 +1,350 @@ +# ============================================================================= +# SurfSense — One-line Install Script (Windows / PowerShell) +# +# +# Usage: irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex +# +# To pass flags, save and run locally: +# .\install.ps1 -NoWatchtower +# .\install.ps1 -WatchtowerInterval 3600 +# +# Handles two cases automatically: +# 1. Fresh install — no prior SurfSense data detected +# 2. Migration from the legacy all-in-one container (surfsense-data volume) +# Downloads and runs migrate-database.sh --yes, then restores the dump +# into the new PostgreSQL 17 stack. The user runs one command for both. +# ============================================================================= + +param( + [switch]$NoWatchtower, + [int]$WatchtowerInterval = 86400 +) + +$ErrorActionPreference = 'Stop' + +# ── Configuration ─────────────────────────────────────────────────────────── + +$RepoRaw = "https://raw.githubusercontent.com/MODSetter/SurfSense/main" +$InstallDir = ".\surfsense" +$OldVolume = "surfsense-data" +$DumpFile = ".\surfsense_migration_backup.sql" +$KeyFile = ".\surfsense_migration_secret.key" +$MigrationDoneFile = "$InstallDir\.migration_done" +$MigrationMode = $false +$SetupWatchtower = -not $NoWatchtower +$WatchtowerContainer = "watchtower" + +# ── Output helpers ────────────────────────────────────────────────────────── + +function Write-Info { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Cyan -NoNewline; Write-Host $Msg } +function Write-Ok { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Green -NoNewline; Write-Host $Msg } +function Write-Warn { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Yellow -NoNewline; Write-Host $Msg } +function Write-Step { param([string]$Msg) Write-Host "`n-- $Msg" -ForegroundColor Cyan } +function Write-Err { param([string]$Msg) Write-Host "[SurfSense] ERROR: $Msg" -ForegroundColor Red; exit 1 } + +function Invoke-NativeSafe { + param([scriptblock]$Command) + $previousErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = 'Continue' + & $Command + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } +} + +# ── Pre-flight checks ────────────────────────────────────────────────────── + +Write-Step "Checking prerequisites" + +if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + Write-Err "Docker is not installed. Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/" +} +Write-Ok "Docker found." + +Invoke-NativeSafe { docker info *>$null } | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Err "Docker daemon is not running. Please start Docker Desktop and try again." +} +Write-Ok "Docker daemon is running." + +Invoke-NativeSafe { docker compose version *>$null } | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Err "Docker Compose is not available. It should be bundled with Docker Desktop." +} +Write-Ok "Docker Compose found." + +# ── Wait-for-postgres helper ──────────────────────────────────────────────── + +function Wait-ForPostgres { + param([string]$DbUser) + $maxAttempts = 45 + $attempt = 0 + + Write-Info "Waiting for PostgreSQL to accept connections..." + do { + $attempt++ + if ($attempt -ge $maxAttempts) { + Write-Err "PostgreSQL did not become ready after $($maxAttempts * 2) seconds.`nCheck logs: cd $InstallDir; docker compose logs db" + } + Start-Sleep -Seconds 2 + Push-Location $InstallDir + Invoke-NativeSafe { docker compose exec -T db pg_isready -U $DbUser -q *>$null } | Out-Null + $ready = $LASTEXITCODE -eq 0 + Pop-Location + } while (-not $ready) + + Write-Ok "PostgreSQL is ready." +} + +# ── Download files ────────────────────────────────────────────────────────── + +Write-Step "Downloading SurfSense files" +Write-Info "Installation directory: $InstallDir" + +New-Item -ItemType Directory -Path "$InstallDir\scripts" -Force | Out-Null + +$Files = @( + @{ Src = "docker/docker-compose.yml"; Dest = "docker-compose.yml" } + @{ Src = "docker/.env.example"; Dest = ".env.example" } + @{ Src = "docker/postgresql.conf"; Dest = "postgresql.conf" } + @{ Src = "docker/scripts/init-electric-user.sh"; Dest = "scripts/init-electric-user.sh" } + @{ Src = "docker/scripts/migrate-database.ps1"; Dest = "scripts/migrate-database.ps1" } +) + +foreach ($f in $Files) { + $destPath = Join-Path $InstallDir $f.Dest + Write-Info "Downloading $($f.Dest)..." + try { + Invoke-WebRequest -Uri "$RepoRaw/$($f.Src)" -OutFile $destPath -UseBasicParsing + } catch { + Write-Err "Failed to download $($f.Dest). Check your internet connection and try again." + } +} + +Write-Ok "All files downloaded to $InstallDir/" + +# ── Legacy all-in-one detection ───────────────────────────────────────────── + +$volumeList = Invoke-NativeSafe { docker volume ls --format '{{.Name}}' 2>$null } +if (($volumeList -split "`n") -contains $OldVolume -and -not (Test-Path $MigrationDoneFile)) { + $MigrationMode = $true + + if (Test-Path $DumpFile) { + Write-Step "Migration mode - using existing dump (skipping extraction)" + Write-Info "Found existing dump: $DumpFile" + Write-Info "Skipping data extraction - proceeding directly to restore." + Write-Info "To force a fresh extraction, remove the dump first: Remove-Item $DumpFile" + } else { + Write-Step "Migration mode - legacy all-in-one container detected" + Write-Warn "Volume '$OldVolume' found. Your data will be migrated automatically." + Write-Warn "PostgreSQL is being upgraded from version 14 to 17." + Write-Warn "Your original data will NOT be deleted." + Write-Host "" + Write-Info "Running data extraction (migrate-database.ps1 -Yes)..." + Write-Info "Full extraction log: ./surfsense-migration.log" + Write-Host "" + + $migrateScript = Join-Path $InstallDir "scripts/migrate-database.ps1" + & $migrateScript -Yes + if ($LASTEXITCODE -ne 0) { + Write-Err "Data extraction failed. See ./surfsense-migration.log for details.`nYou can also run migrate-database.ps1 manually with custom flags." + } + + Write-Host "" + Write-Ok "Data extraction complete. Proceeding with installation and restore." + } +} + +# ── Set up .env ───────────────────────────────────────────────────────────── + +Write-Step "Configuring environment" + +$envPath = Join-Path $InstallDir ".env" +$envExamplePath = Join-Path $InstallDir ".env.example" + +if (-not (Test-Path $envPath)) { + Copy-Item $envExamplePath $envPath + + if ($MigrationMode -and (Test-Path $KeyFile)) { + $SecretKey = (Get-Content $KeyFile -Raw).Trim() + Write-Ok "Using SECRET_KEY recovered from legacy container." + } else { + $bytes = New-Object byte[] 32 + $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() + $rng.GetBytes($bytes) + $rng.Dispose() + $SecretKey = [Convert]::ToBase64String($bytes) + Write-Ok "Generated new random SECRET_KEY." + } + + $content = Get-Content $envPath -Raw + $content = $content -replace 'SECRET_KEY=replace_me_with_a_random_string', "SECRET_KEY=$SecretKey" + Set-Content -Path $envPath -Value $content -NoNewline + + Write-Info "Created $envPath" +} else { + Write-Warn ".env already exists - keeping your existing configuration." +} + +# ── Start containers ──────────────────────────────────────────────────────── + +if ($MigrationMode) { + $envContent = Get-Content $envPath + $DbUser = ($envContent | Select-String '^DB_USER=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1 + $DbPass = ($envContent | Select-String '^DB_PASSWORD=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1 + $DbName = ($envContent | Select-String '^DB_NAME=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1 + if (-not $DbUser) { $DbUser = "surfsense" } + if (-not $DbPass) { $DbPass = "surfsense" } + if (-not $DbName) { $DbName = "surfsense" } + + Write-Step "Starting PostgreSQL 17" + Push-Location $InstallDir + Invoke-NativeSafe { docker compose up -d db } | Out-Null + Pop-Location + Wait-ForPostgres -DbUser $DbUser + + Write-Step "Restoring database" + if (-not (Test-Path $DumpFile)) { + Write-Err "Dump file '$DumpFile' not found. The migration script may have failed." + } + $DumpFilePath = (Resolve-Path $DumpFile).Path + Write-Info "Restoring dump into PostgreSQL 17 - this may take a while for large databases..." + + $restoreErrFile = Join-Path $env:TEMP "surfsense_restore_err.log" + Push-Location $InstallDir + Invoke-NativeSafe { Get-Content -LiteralPath $DumpFilePath | docker compose exec -T -e "PGPASSWORD=$DbPass" db psql -U $DbUser -d $DbName 2>$restoreErrFile | Out-Null } | Out-Null + Pop-Location + + $fatalErrors = @() + if (Test-Path $restoreErrFile) { + $fatalErrors = Get-Content $restoreErrFile | + Where-Object { $_ -match '^ERROR:' } | + Where-Object { $_ -notmatch 'already exists' } | + Where-Object { $_ -notmatch 'multiple primary keys' } + } + + if ($fatalErrors.Count -gt 0) { + Write-Warn "Restore completed with errors (may be harmless pg_dump header noise):" + $fatalErrors | ForEach-Object { Write-Host $_ } + Write-Warn "If SurfSense behaves incorrectly, inspect manually." + } else { + Write-Ok "Database restored with no fatal errors." + } + + # Smoke test + Push-Location $InstallDir + $tableCount = (Invoke-NativeSafe { docker compose exec -T -e "PGPASSWORD=$DbPass" db psql -U $DbUser -d $DbName -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>$null }).Trim() + Pop-Location + + if (-not $tableCount -or $tableCount -eq "0") { + Write-Warn "Smoke test: no tables found after restore." + Write-Warn "The restore may have failed silently. Check: cd $InstallDir; docker compose logs db" + } else { + Write-Ok "Smoke test passed: $tableCount table(s) restored successfully." + New-Item -Path $MigrationDoneFile -ItemType File -Force | Out-Null + } + + Write-Step "Starting all SurfSense services" + Push-Location $InstallDir + Invoke-NativeSafe { docker compose up -d } + Pop-Location + Write-Ok "All services started." + + Remove-Item $KeyFile -ErrorAction SilentlyContinue + +} else { + Write-Step "Starting SurfSense" + Push-Location $InstallDir + Invoke-NativeSafe { docker compose up -d } + Pop-Location + Write-Ok "All services started." +} + +# ── Watchtower (auto-update) ──────────────────────────────────────────────── + +if ($SetupWatchtower) { + $wtHours = [math]::Floor($WatchtowerInterval / 3600) + Write-Step "Setting up Watchtower (auto-updates every ${wtHours}h)" + + $wtState = Invoke-NativeSafe { docker inspect -f '{{.State.Running}}' $WatchtowerContainer 2>$null } + if ($LASTEXITCODE -ne 0) { $wtState = "missing" } + + if ($wtState -eq "true") { + Write-Ok "Watchtower is already running - skipping." + } else { + if ($wtState -ne "missing") { + Write-Info "Removing stopped Watchtower container..." + Invoke-NativeSafe { docker rm -f $WatchtowerContainer *>$null } | Out-Null + } + Invoke-NativeSafe { + docker run -d ` + --name $WatchtowerContainer ` + --restart unless-stopped ` + -v /var/run/docker.sock:/var/run/docker.sock ` + nickfedor/watchtower ` + --label-enable ` + --interval $WatchtowerInterval *>$null + } | Out-Null + + if ($LASTEXITCODE -eq 0) { + Write-Ok "Watchtower started - labeled SurfSense containers will auto-update." + } else { + Write-Warn "Could not start Watchtower. You can set it up manually or use: docker compose pull; docker compose up -d" + } + } +} else { + Write-Info "Skipping Watchtower setup (-NoWatchtower flag)." +} + +# ── Done ──────────────────────────────────────────────────────────────────── + +Write-Host "" +Write-Host @" + + + .d8888b. .d888 .d8888b. +d88P Y88b d88P" d88P Y88b +Y88b. 888 Y88b. + "Y888b. 888 888 888d888 888888 "Y888b. .d88b. 88888b. .d8888b .d88b. + "Y88b. 888 888 888P" 888 "Y88b. d8P Y8b 888 "88b 88K d8P Y8b + "888 888 888 888 888 "888 88888888 888 888 "Y8888b. 88888888 +Y88b d88P Y88b 888 888 888 Y88b d88P Y8b. 888 888 X88 Y8b. + "Y8888P" "Y88888 888 888 "Y8888P" "Y8888 888 888 88888P' "Y8888 + + +"@ -ForegroundColor White + +$versionDisplay = (Get-Content $envPath | Select-String '^SURFSENSE_VERSION=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1 +if (-not $versionDisplay) { $versionDisplay = "latest" } +Write-Host " OSS Alternative to NotebookLM for Teams [$versionDisplay]" -ForegroundColor Yellow +Write-Host ("=" * 62) -ForegroundColor Cyan +Write-Host "" + +Write-Info " Frontend: http://localhost:3000" +Write-Info " Backend: http://localhost:8000" +Write-Info " API Docs: http://localhost:8000/docs" +Write-Info "" +Write-Info " Config: $InstallDir\.env" +Write-Info " Logs: cd $InstallDir; docker compose logs -f" +Write-Info " Stop: cd $InstallDir; docker compose down" +Write-Info " Update: cd $InstallDir; docker compose pull; docker compose up -d" +Write-Info "" + +if ($SetupWatchtower) { + Write-Info " Watchtower: auto-updates every ${wtHours}h (stop: docker rm -f $WatchtowerContainer)" +} else { + Write-Warn " Watchtower skipped. For auto-updates, re-run without -NoWatchtower." +} +Write-Info "" + +if ($MigrationMode) { + Write-Warn " Migration complete! Open frontend and verify your data." + Write-Warn " Once verified, clean up the legacy volume and migration files:" + Write-Warn " docker volume rm $OldVolume" + Write-Warn " Remove-Item $DumpFile" + Write-Warn " Remove-Item $MigrationDoneFile" +} else { + Write-Warn " First startup may take a few minutes while images are pulled." + Write-Warn " Edit $InstallDir\.env to configure API keys, OAuth, etc." +} diff --git a/docker/scripts/install.sh b/docker/scripts/install.sh index 74697a454..c4a0d5c9f 100644 --- a/docker/scripts/install.sh +++ b/docker/scripts/install.sh @@ -25,7 +25,7 @@ set -euo pipefail main() { -REPO_RAW="https://raw.githubusercontent.com/MODSetter/SurfSense/dev" +REPO_RAW="https://raw.githubusercontent.com/MODSetter/SurfSense/main" INSTALL_DIR="./surfsense" OLD_VOLUME="surfsense-data" DUMP_FILE="./surfsense_migration_backup.sql" @@ -299,12 +299,9 @@ Y88b d88P Y88b 888 888 888 Y88b d88P Y8b. 888 888 X88 Y8b. EOF -if [[ "${SURFSENSE_VERSION:-latest}" == "latest" ]]; then - _version_display="latest" -else - _version_display="v${SURFSENSE_VERSION}" -fi -printf " Your personal AI-powered search engine ${YELLOW}[%s]${NC}\n" "${_version_display}" +_version_display=$(grep '^SURFSENSE_VERSION=' "${INSTALL_DIR}/.env" 2>/dev/null | cut -d= -f2 | tr -d '"' | head -1 || true) +_version_display="${_version_display:-latest}" +printf " OSS Alternative to NotebookLM for Teams ${YELLOW}[%s]${NC}\n" "${_version_display}" printf "${CYAN}══════════════════════════════════════════════════════════════${NC}\n\n" info " Frontend: http://localhost:3000" diff --git a/docker/scripts/migrate-database.ps1 b/docker/scripts/migrate-database.ps1 new file mode 100644 index 000000000..56769fcaf --- /dev/null +++ b/docker/scripts/migrate-database.ps1 @@ -0,0 +1,343 @@ +# ============================================================================= +# SurfSense — Database Migration Script (Windows / PowerShell) +# +# Extracts data from the legacy all-in-one surfsense-data volume (PostgreSQL 14) +# and saves it as a SQL dump + SECRET_KEY file ready for install.ps1 to restore. +# +# Usage: +# .\migrate-database.ps1 [options] +# +# Options: +# -DbUser USER Old PostgreSQL username (default: surfsense) +# -DbPassword PASS Old PostgreSQL password (default: surfsense) +# -DbName NAME Old PostgreSQL database (default: surfsense) +# -Yes Skip all confirmation prompts +# +# Prerequisites: +# - Docker Desktop installed and running +# - The legacy surfsense-data volume must exist +# - ~500 MB free disk space for the dump file +# +# What this script does: +# 1. Stops any container using surfsense-data (to prevent corruption) +# 2. Starts a temporary PG14 container against the old volume +# 3. Dumps the database to .\surfsense_migration_backup.sql +# 4. Recovers the SECRET_KEY to .\surfsense_migration_secret.key +# 5. Exits — leaving installation to install.ps1 +# +# What this script does NOT do: +# - Delete the original surfsense-data volume (do this manually after verifying) +# - Install the new SurfSense stack (install.ps1 handles that automatically) +# +# Note: +# install.ps1 downloads and runs this script automatically when it detects the +# legacy surfsense-data volume. You only need to run this script manually if +# you have custom database credentials (-DbUser / -DbPassword / -DbName) +# or if the automatic migration inside install.ps1 fails at the extraction step. +# ============================================================================= + +param( + [string]$DbUser = "surfsense", + [string]$DbPassword = "surfsense", + [string]$DbName = "surfsense", + [switch]$Yes +) + +$ErrorActionPreference = 'Stop' + +# ── Constants ──────────────────────────────────────────────────────────────── + +$OldVolume = "surfsense-data" +$TempContainer = "surfsense-pg14-migration" +$DumpFile = ".\surfsense_migration_backup.sql" +$KeyFile = ".\surfsense_migration_secret.key" +$PG14Image = "pgvector/pgvector:pg14" +$LogFile = ".\surfsense-migration.log" + +# ── Output helpers ─────────────────────────────────────────────────────────── + +function Write-Info { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Cyan -NoNewline; Write-Host $Msg } +function Write-Ok { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Green -NoNewline; Write-Host $Msg } +function Write-Warn { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Yellow -NoNewline; Write-Host $Msg } +function Write-Step { param([string]$Step, [string]$Msg) Write-Host "`n-- Step ${Step}: $Msg" -ForegroundColor Cyan } +function Write-Err { param([string]$Msg) Write-Host "[SurfSense] ERROR: $Msg" -ForegroundColor Red; exit 1 } + +function Log { param([string]$Msg) Add-Content -Path $LogFile -Value $Msg } + +function Invoke-NativeSafe { + param([scriptblock]$Command) + $previousErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = 'Continue' + & $Command + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } +} + +function Confirm-Action { + param([string]$Prompt) + if ($Yes) { return } + $reply = Read-Host "[SurfSense] $Prompt [y/N]" + if ($reply -notmatch '^[Yy]$') { + Write-Warn "Aborted." + exit 0 + } +} + +# ── Cleanup helper ─────────────────────────────────────────────────────────── + +function Remove-TempContainer { + $containers = Invoke-NativeSafe { docker ps -a --format '{{.Names}}' 2>$null } + if ($containers -and ($containers -split "`n") -contains $TempContainer) { + Write-Info "Cleaning up temporary container '$TempContainer'..." + Invoke-NativeSafe { docker stop $TempContainer *>$null } | Out-Null + Invoke-NativeSafe { docker rm $TempContainer *>$null } | Out-Null + } +} + +# Register cleanup on script exit +Register-EngineEvent PowerShell.Exiting -Action { + $containers = Invoke-NativeSafe { docker ps -a --format '{{.Names}}' 2>$null } + if ($containers -and ($containers -split "`n") -contains "surfsense-pg14-migration") { + Invoke-NativeSafe { docker stop "surfsense-pg14-migration" *>$null } | Out-Null + Invoke-NativeSafe { docker rm "surfsense-pg14-migration" *>$null } | Out-Null + } +} | Out-Null + +# ── Wait-for-postgres helper ──────────────────────────────────────────────── + +function Wait-ForPostgres { + param( + [string]$Container, + [string]$User, + [string]$Label = "PostgreSQL" + ) + $maxAttempts = 45 + $attempt = 0 + + Write-Info "Waiting for $Label to accept connections..." + do { + $attempt++ + if ($attempt -ge $maxAttempts) { + Write-Err "$Label did not become ready after $($maxAttempts * 2) seconds. Check: docker logs $Container" + } + Start-Sleep -Seconds 2 + Invoke-NativeSafe { docker exec $Container pg_isready -U $User -q 2>$null } | Out-Null + } while ($LASTEXITCODE -ne 0) + + Write-Ok "$Label is ready." +} + +Write-Info "Migrating data from legacy database (PostgreSQL 14 -> 17)" +"Migration started at $(Get-Date)" | Out-File $LogFile + +# ── Step 0: Pre-flight checks ─────────────────────────────────────────────── + +Write-Step "0" "Pre-flight checks" + +if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + Write-Err "Docker is not installed. Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/" +} + +Invoke-NativeSafe { docker info *>$null } | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Err "Docker daemon is not running. Please start Docker Desktop and try again." +} + +$volumeList = Invoke-NativeSafe { docker volume ls --format '{{.Name}}' 2>$null } +if (-not (($volumeList -split "`n") -contains $OldVolume)) { + Write-Err "Legacy volume '$OldVolume' not found. Are you sure you ran the old all-in-one SurfSense container?" +} +Write-Ok "Found legacy volume: $OldVolume" + +$oldContainer = (Invoke-NativeSafe { docker ps --filter "volume=$OldVolume" --format '{{.Names}}' 2>$null } | Select-Object -First 1) +if ($oldContainer) { + Write-Warn "Container '$oldContainer' is running and using the '$OldVolume' volume." + Write-Warn "It must be stopped before migration to prevent data file corruption." + Confirm-Action "Stop '$oldContainer' now and proceed with data extraction?" + Invoke-NativeSafe { docker stop $oldContainer *>$null } | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Err "Failed to stop '$oldContainer'. Try: docker stop $oldContainer" + } + Write-Ok "Container '$oldContainer' stopped." +} + +if (Test-Path $DumpFile) { + Write-Warn "Dump file '$DumpFile' already exists." + Write-Warn "If a previous extraction succeeded, just run install.ps1 now." + Write-Warn "To re-extract, remove the file first: Remove-Item $DumpFile" + Write-Err "Aborting to avoid overwriting an existing dump." +} + +$staleContainers = Invoke-NativeSafe { docker ps -a --format '{{.Names}}' 2>$null } +if ($staleContainers -and ($staleContainers -split "`n") -contains $TempContainer) { + Write-Warn "Stale migration container '$TempContainer' found - removing it." + Invoke-NativeSafe { docker stop $TempContainer *>$null } | Out-Null + Invoke-NativeSafe { docker rm $TempContainer *>$null } | Out-Null +} + +$drive = (Get-Item .).PSDrive +$freeMB = [math]::Floor($drive.Free / 1MB) +if ($freeMB -lt 500) { + Write-Warn "Low disk space: $freeMB MB free. At least 500 MB recommended for the dump." + Confirm-Action "Continue anyway?" +} else { + Write-Ok "Disk space: $freeMB MB free." +} + +Write-Ok "All pre-flight checks passed." + +# ── Confirmation prompt ────────────────────────────────────────────────────── + +Write-Host "" +Write-Host "Extraction plan:" -ForegroundColor White +Write-Host " Source volume : " -NoNewline; Write-Host "$OldVolume" -ForegroundColor Yellow -NoNewline; Write-Host " (PG14 data at /data/postgres)" +Write-Host " Old credentials : user=" -NoNewline; Write-Host "$DbUser" -ForegroundColor Yellow -NoNewline; Write-Host " db=" -NoNewline; Write-Host "$DbName" -ForegroundColor Yellow +Write-Host " Dump saved to : " -NoNewline; Write-Host "$DumpFile" -ForegroundColor Yellow +Write-Host " SECRET_KEY to : " -NoNewline; Write-Host "$KeyFile" -ForegroundColor Yellow +Write-Host " Log file : " -NoNewline; Write-Host "$LogFile" -ForegroundColor Yellow +Write-Host "" +Confirm-Action "Start data extraction? (Your original data will not be deleted or modified.)" + +# ── Step 1: Start temporary PostgreSQL 14 container ────────────────────────── + +Write-Step "1" "Starting temporary PostgreSQL 14 container" + +Write-Info "Pulling $PG14Image..." +Invoke-NativeSafe { docker pull $PG14Image *>$null } | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Warn "Could not pull $PG14Image - using cached image if available." +} + +$dataUid = Invoke-NativeSafe { docker run --rm -v "${OldVolume}:/data" alpine stat -c '%u' /data/postgres 2>$null } +if (-not $dataUid -or $dataUid -eq "0") { + Write-Warn "Could not detect data directory UID - falling back to default (may chown files)." + $userFlag = @() +} else { + Write-Info "Data directory owned by UID $dataUid - starting temp container as that user." + $userFlag = @("--user", $dataUid) +} + +$dockerRunArgs = @( + "run", "-d", + "--name", $TempContainer, + "-v", "${OldVolume}:/data", + "-e", "PGDATA=/data/postgres", + "-e", "POSTGRES_USER=$DbUser", + "-e", "POSTGRES_PASSWORD=$DbPassword", + "-e", "POSTGRES_DB=$DbName" +) + $userFlag + @($PG14Image) + +Invoke-NativeSafe { docker @dockerRunArgs *>$null } | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Err "Failed to start temporary PostgreSQL 14 container." +} + +Write-Ok "Temporary container '$TempContainer' started." +Wait-ForPostgres -Container $TempContainer -User $DbUser -Label "PostgreSQL 14" + +# ── Step 2: Dump the database ──────────────────────────────────────────────── + +Write-Step "2" "Dumping PostgreSQL 14 database" + +Write-Info "Running pg_dump - this may take a while for large databases..." + +$pgDumpErrFile = Join-Path $env:TEMP "surfsense_pgdump_err.log" +Invoke-NativeSafe { docker exec -e "PGPASSWORD=$DbPassword" $TempContainer pg_dump -U $DbUser --no-password $DbName > $DumpFile 2>$pgDumpErrFile } | Out-Null +if ($LASTEXITCODE -ne 0) { + if (Test-Path $pgDumpErrFile) { Get-Content $pgDumpErrFile | Write-Host -ForegroundColor Red } + Remove-TempContainer + Write-Err "pg_dump failed. See above for details." +} + +if (-not (Test-Path $DumpFile) -or (Get-Item $DumpFile).Length -eq 0) { + Remove-TempContainer + Write-Err "Dump file '$DumpFile' is empty. Something went wrong with pg_dump." +} + +$dumpContent = (Get-Content $DumpFile -TotalCount 5) -join "`n" +if ($dumpContent -notmatch "PostgreSQL database dump") { + Remove-TempContainer + Write-Err "Dump file does not contain a valid PostgreSQL dump header - the file may be corrupt." +} + +$dumpLines = (Get-Content $DumpFile | Measure-Object -Line).Lines +if ($dumpLines -lt 10) { + Remove-TempContainer + Write-Err "Dump has only $dumpLines lines - suspiciously small. Aborting." +} + +$dumpSize = "{0:N1} MB" -f ((Get-Item $DumpFile).Length / 1MB) +Write-Ok "Dump complete: $dumpSize ($dumpLines lines) -> $DumpFile" + +Write-Info "Stopping temporary PostgreSQL 14 container..." +Invoke-NativeSafe { docker stop $TempContainer *>$null } | Out-Null +Invoke-NativeSafe { docker rm $TempContainer *>$null } | Out-Null +Write-Ok "Temporary container removed." + +# ── Step 3: Recover SECRET_KEY ─────────────────────────────────────────────── + +Write-Step "3" "Recovering SECRET_KEY" + +$recoveredKey = "" + +$keyCheck = Invoke-NativeSafe { docker run --rm -v "${OldVolume}:/data" alpine sh -c 'test -f /data/.secret_key && cat /data/.secret_key' 2>$null } +if ($LASTEXITCODE -eq 0 -and $keyCheck) { + $recoveredKey = $keyCheck.Trim() + Write-Ok "Recovered SECRET_KEY from '$OldVolume'." +} else { + Write-Warn "No SECRET_KEY file found at /data/.secret_key in '$OldVolume'." + Write-Warn "This means the all-in-one container was launched with SECRET_KEY set as an explicit env var." + + if ($Yes) { + $bytes = New-Object byte[] 32 + $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() + $rng.GetBytes($bytes) + $rng.Dispose() + $recoveredKey = [Convert]::ToBase64String($bytes) + Write-Warn "Non-interactive mode: generated a new SECRET_KEY automatically." + Write-Warn "All active browser sessions will be logged out after migration." + Write-Warn "To restore your original key, update SECRET_KEY in .\surfsense\.env afterwards." + } else { + Write-Warn "Enter the SECRET_KEY from your old container's environment" + $recoveredKey = Read-Host "[SurfSense] (press Enter to generate a new one - existing sessions will be invalidated)" + if (-not $recoveredKey) { + $bytes = New-Object byte[] 32 + $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() + $rng.GetBytes($bytes) + $rng.Dispose() + $recoveredKey = [Convert]::ToBase64String($bytes) + Write-Warn "Generated a new SECRET_KEY. All active browser sessions will be logged out after migration." + } + } +} + +Set-Content -Path $KeyFile -Value $recoveredKey -NoNewline +Write-Ok "SECRET_KEY saved to $KeyFile" + +# ── Done ───────────────────────────────────────────────────────────────────── + +Write-Host "" +Write-Host ("=" * 62) -ForegroundColor Green +Write-Host " Data extraction complete!" -ForegroundColor Green +Write-Host ("=" * 62) -ForegroundColor Green +Write-Host "" + +Write-Ok "Dump file : $DumpFile ($dumpSize)" +Write-Ok "Secret key: $KeyFile" +Write-Host "" +Write-Info "Next step - run install.ps1 from this same directory:" +Write-Host "" +Write-Host " irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex" -ForegroundColor Cyan +Write-Host "" +Write-Info "install.ps1 will detect the dump, restore your data into PostgreSQL 17," +Write-Info "and start the full SurfSense stack automatically." +Write-Host "" +Write-Warn "Keep both files until you have verified the migration:" +Write-Warn " $DumpFile" +Write-Warn " $KeyFile" +Write-Warn "Full log saved to: $LogFile" +Write-Host "" + +Log "Migration extraction completed successfully at $(Get-Date)" diff --git a/surfsense_backend/app/agents/podcaster/nodes.py b/surfsense_backend/app/agents/podcaster/nodes.py index 3f908737a..4bdfdfc48 100644 --- a/surfsense_backend/app/agents/podcaster/nodes.py +++ b/surfsense_backend/app/agents/podcaster/nodes.py @@ -12,7 +12,7 @@ from litellm import aspeech from app.config import config as app_config from app.services.kokoro_tts_service import get_kokoro_tts_service -from app.services.llm_service import get_document_summary_llm +from app.services.llm_service import get_agent_llm from .configuration import Configuration from .prompts import get_podcast_generation_prompt @@ -31,7 +31,7 @@ async def create_podcast_transcript( user_prompt = configuration.user_prompt # Get search space's document summary LLM - llm = await get_document_summary_llm(state.db_session, search_space_id) + llm = await get_agent_llm(state.db_session, search_space_id) if not llm: error_message = ( f"No document summary LLM configured for search space {search_space_id}" diff --git a/surfsense_browser_extension/routes/index.tsx b/surfsense_browser_extension/routes/index.tsx index 8df110be1..39aed5854 100644 --- a/surfsense_browser_extension/routes/index.tsx +++ b/surfsense_browser_extension/routes/index.tsx @@ -2,7 +2,7 @@ import { Route, Routes } from "react-router-dom"; import ApiKeyForm from "./pages/ApiKeyForm"; import HomePage from "./pages/HomePage"; -import "../tailwind.css"; +import "~tailwind.css"; export const Routing = () => ( diff --git a/surfsense_browser_extension/routes/pages/ApiKeyForm.tsx b/surfsense_browser_extension/routes/pages/ApiKeyForm.tsx index b6deb1c05..537eba3da 100644 --- a/surfsense_browser_extension/routes/pages/ApiKeyForm.tsx +++ b/surfsense_browser_extension/routes/pages/ApiKeyForm.tsx @@ -4,6 +4,8 @@ import { ReloadIcon } from "@radix-ui/react-icons"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Button } from "~/routes/ui/button"; +import { ConnectionSettingsButton } from "~/routes/ui/connection-settings-button"; +import { buildBackendUrl } from "~utils/backend-url"; const ApiKeyForm = () => { const navigation = useNavigate(); @@ -27,8 +29,7 @@ const ApiKeyForm = () => { setLoading(true); try { - // Verify token is valid by making a request to the API - const response = await fetch(`${process.env.PLASMO_PUBLIC_BACKEND_URL}/verify-token`, { + const response = await fetch(await buildBackendUrl("/verify-token"), { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, @@ -53,6 +54,10 @@ const ApiKeyForm = () => { return (
+
+ +
+
SurfSense diff --git a/surfsense_browser_extension/routes/pages/HomePage.tsx b/surfsense_browser_extension/routes/pages/HomePage.tsx index 362c64056..9d8787d29 100644 --- a/surfsense_browser_extension/routes/pages/HomePage.tsx +++ b/surfsense_browser_extension/routes/pages/HomePage.tsx @@ -16,6 +16,7 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { cn } from "~/lib/utils"; import { Button } from "~/routes/ui/button"; +import { ConnectionSettingsButton } from "~/routes/ui/connection-settings-button"; import { Command, CommandEmpty, @@ -27,6 +28,7 @@ import { import { Popover, PopoverContent, PopoverTrigger } from "~/routes/ui/popover"; import { Label } from "~routes/ui/label"; import { useToast } from "~routes/ui/use-toast"; +import { buildBackendUrl } from "~utils/backend-url"; import { getRenderedHtml } from "~utils/commons"; import type { WebHistory } from "~utils/interfaces"; import Loading from "./Loading"; @@ -45,15 +47,19 @@ const HomePage = () => { const checkSearchSpaces = async () => { const storage = new Storage({ area: "local" }); const token = await storage.get("token"); + + if (!token) { + setLoading(false); + navigation("/login"); + return; + } + try { - const response = await fetch( - `${process.env.PLASMO_PUBLIC_BACKEND_URL}/api/v1/searchspaces`, - { - headers: { - Authorization: `Bearer ${token}`, - }, + const response = await fetch(await buildBackendUrl("/api/v1/searchspaces"), { + headers: { + Authorization: `Bearer ${token}`, } - ); + }); if (!response.ok) { throw new Error("Token verification failed"); @@ -66,11 +72,12 @@ const HomePage = () => { await storage.remove("token"); await storage.remove("showShadowDom"); navigation("/login"); + } finally { + setLoading(false); } }; checkSearchSpaces(); - setLoading(false); }, []); useEffect(() => { @@ -304,6 +311,19 @@ const HomePage = () => { navigation("/login"); } + async function handleConnectionSaved(changed: boolean): Promise { + if (!changed) { + return; + } + + const storage = new Storage({ area: "local" }); + await storage.remove("token"); + await storage.remove("showShadowDom"); + await storage.remove("search_space"); + await storage.remove("search_space_id"); + navigation("/login"); + } + if (loading) { return ; } else { @@ -344,15 +364,18 @@ const HomePage = () => {

SurfSense

- +
+ + +
diff --git a/surfsense_browser_extension/routes/ui/connection-settings-button.tsx b/surfsense_browser_extension/routes/ui/connection-settings-button.tsx new file mode 100644 index 000000000..f68f252a5 --- /dev/null +++ b/surfsense_browser_extension/routes/ui/connection-settings-button.tsx @@ -0,0 +1,114 @@ +import { GearIcon } from "@radix-ui/react-icons"; +import { useEffect, useState } from "react"; +import { Button } from "~/routes/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "~/routes/ui/dialog"; +import { Label } from "~/routes/ui/label"; +import { + DEFAULT_BACKEND_BASE_URL, + getCustomBackendBaseUrl, + normalizeBackendBaseUrl, + setCustomBackendBaseUrl, +} from "~utils/backend-url"; + +type ConnectionSettingsButtonProps = { + onSaved?: (changed: boolean) => void | Promise; +}; + +export function ConnectionSettingsButton({ onSaved }: ConnectionSettingsButtonProps) { + const [open, setOpen] = useState(false); + const [customUrl, setCustomUrl] = useState(""); + const [savedUrl, setSavedUrl] = useState(""); + + useEffect(() => { + if (!open) { + return; + } + + const loadSettings = async () => { + const normalized = await getCustomBackendBaseUrl(); + setCustomUrl(normalized || DEFAULT_BACKEND_BASE_URL); + setSavedUrl(normalized); + }; + + loadSettings(); + }, [open]); + + const handleSave = async () => { + const normalizedUrl = normalizeBackendBaseUrl(customUrl); + const nextUrl = await setCustomBackendBaseUrl( + normalizedUrl === DEFAULT_BACKEND_BASE_URL ? "" : normalizedUrl + ); + const changed = nextUrl !== savedUrl; + setSavedUrl(nextUrl); + setCustomUrl(nextUrl || DEFAULT_BACKEND_BASE_URL); + setOpen(false); + + if (onSaved) { + await onSaved(changed); + } + }; + + return ( + <> + + + + + Connection Settings + + Leave blank to use the default SurfSense backend URL. + + + +
+ + setCustomUrl(event.target.value)} + placeholder={DEFAULT_BACKEND_BASE_URL} + className="w-full rounded-md border border-gray-700 bg-gray-900 px-3 py-2 text-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-teal-500" + /> +

Default: {DEFAULT_BACKEND_BASE_URL}

+
+ + + + + +
+
+ + ); +} diff --git a/surfsense_browser_extension/utils/backend-url.ts b/surfsense_browser_extension/utils/backend-url.ts new file mode 100644 index 000000000..b295bf963 --- /dev/null +++ b/surfsense_browser_extension/utils/backend-url.ts @@ -0,0 +1,41 @@ +import { Storage } from "@plasmohq/storage"; + +export const BACKEND_URL_STORAGE_KEY = "backend_base_url"; +export const FALLBACK_BACKEND_BASE_URL = "https://www.surfsense.com"; + +const storage = new Storage({ area: "local" }); + +export function normalizeBackendBaseUrl(url: string) { + return url.trim().replace(/\/+$/, ""); +} + +export const DEFAULT_BACKEND_BASE_URL = normalizeBackendBaseUrl( + process.env.PLASMO_PUBLIC_BACKEND_URL || FALLBACK_BACKEND_BASE_URL +); + +export async function getCustomBackendBaseUrl() { + const value = await storage.get(BACKEND_URL_STORAGE_KEY); + return typeof value === "string" ? normalizeBackendBaseUrl(value) : ""; +} + +export async function setCustomBackendBaseUrl(url: string) { + const normalized = normalizeBackendBaseUrl(url); + + if (normalized) { + await storage.set(BACKEND_URL_STORAGE_KEY, normalized); + return normalized; + } + + await storage.remove(BACKEND_URL_STORAGE_KEY); + return ""; +} + +export async function getBackendBaseUrl() { + return (await getCustomBackendBaseUrl()) || DEFAULT_BACKEND_BASE_URL; +} + +export async function buildBackendUrl(path: string) { + const baseUrl = await getBackendBaseUrl(); + const normalizedPath = path.startsWith("/") ? path : `/${path}`; + return `${baseUrl}${normalizedPath}`; +} diff --git a/surfsense_web/app/verify-token/route.ts b/surfsense_web/app/verify-token/route.ts new file mode 100644 index 000000000..1c11d6ce0 --- /dev/null +++ b/surfsense_web/app/verify-token/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from "next/server"; + +const backendBaseUrl = (process.env.INTERNAL_FASTAPI_BACKEND_URL || "http://backend:8000").replace( + /\/+$/, + "" +); + +export async function GET(request: NextRequest) { + const response = await fetch(`${backendBaseUrl}/verify-token`, { + method: "GET", + headers: { + Authorization: request.headers.get("authorization") || "", + "X-API-Key": request.headers.get("x-api-key") || "", + }, + cache: "no-store", + }); + + return new NextResponse(response.body, { + status: response.status, + headers: { + "content-type": response.headers.get("content-type") || "application/json", + "cache-control": "no-store", + }, + }); +} diff --git a/surfsense_web/content/docs/docker-installation.mdx b/surfsense_web/content/docs/docker-installation.mdx index d6a6bca85..043405609 100644 --- a/surfsense_web/content/docs/docker-installation.mdx +++ b/surfsense_web/content/docs/docker-installation.mdx @@ -12,14 +12,20 @@ This guide explains how to run SurfSense using Docker, with options ranging from Downloads the compose files, generates a `SECRET_KEY`, starts all services, and sets up [Watchtower](https://github.com/nicholas-fedor/watchtower) for automatic daily updates. - -Windows users: install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) first and run the command below in the Ubuntu terminal. - +**Prerequisites:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) must be installed and running. + +#### For Linux/macOS users: ```bash curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash ``` +#### For Windows users (PowerShell): + +```powershell +irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex +``` + This creates a `./surfsense/` directory with `docker-compose.yml` and `.env`, then runs `docker compose up -d`. To skip Watchtower (e.g. in production where you manage updates yourself):