diff --git a/.env.example b/.env.example index 5ece4e2..a2ef39b 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ EXPO_PUBLIC_USE_LOCAL_API=1 EXPO_PUBLIC_LOCAL_API_URL=http://192.168.76.129:8080 -EXPO_PUBLIC_API_URL=https://devbits.ddns.net +EXPO_PUBLIC_API_URL=https://devbits.app EXPO_PUBLIC_LOCAL_API_PORT=8080 POSTGRES_DB=devbits_dev POSTGRES_USER=devbits_dev -POSTGRES_PASSWORD=devbits_dev_password \ No newline at end of file +POSTGRES_PASSWORD=devbits_dev_password diff --git a/Bash-Scripts/run-dev.sh b/Bash-Scripts/run-dev.sh index cbf2078..1011e8b 100644 --- a/Bash-Scripts/run-dev.sh +++ b/Bash-Scripts/run-dev.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # Script: run-dev.sh -# Does: Boots local dev DB + local backend (isolated compose project), then launches frontend in local mode. -# Use: ./run-dev.sh [--clear] +# Does: Boots local dev DB + local backend (isolated compose project) only. +# Use: ./run-dev.sh # DB: devbits_dev (user/pass: devbits_dev/devbits_dev_password) in compose project devbits-dev-local. # Ports: backend default :8080, DB default :5433 (DEVBITS_BACKEND_PORT / DEVBITS_DB_PORT override). -# Modes: Frontend=ON(local API) | Backend=ON(local Docker) | Live stack untouched | Test DB untouched. +# Modes: Frontend=OFF | Backend=ON(local Docker) | Live stack untouched | Test DB untouched. set -euo pipefail @@ -14,9 +14,9 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" BACKEND_DIR="$ROOT/backend" COMPOSE_PROJECT="devbits-dev-local" -CLEAR_FRONTEND="" -if [[ "${1:-}" == "--clear" ]]; then - CLEAR_FRONTEND="--clear" +if [[ "${EUID}" -eq 0 ]]; then + echo "Warning: Running as root is not recommended for Expo local mode." + echo "Use ./run-dev.sh (without sudo) so LAN IP detection and device connectivity work reliably." fi if ! command -v docker >/dev/null 2>&1; then @@ -109,6 +109,10 @@ for i in $(seq 1 60); do sleep 1 done -echo "Launching frontend in local backend mode..." -cd "$ROOT" -EXPO_PUBLIC_LOCAL_API_PORT="$DEVBITS_BACKEND_PORT" "$SCRIPT_DIR/run-front.sh" --local $CLEAR_FRONTEND \ No newline at end of file +echo +echo "Local backend stack is ready." +echo "Backend health: http://localhost:${DEVBITS_BACKEND_PORT}/health" +echo "To launch frontend against local backend, run:" +echo " EXPO_PUBLIC_LOCAL_API_PORT=${DEVBITS_BACKEND_PORT} $SCRIPT_DIR/run-front.sh --local" +echo "To launch frontend against live backend, run:" +echo " $SCRIPT_DIR/run-front.sh --live" \ No newline at end of file diff --git a/Bash-Scripts/run-front.sh b/Bash-Scripts/run-front.sh index d3cab0e..e5c0e99 100644 --- a/Bash-Scripts/run-front.sh +++ b/Bash-Scripts/run-front.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # Script: run-front.sh -# Does: Starts Expo frontend and lets you choose backend target (Production or Local). -# Use: ./run-front.sh [--local|--production] [--clear] [--dev-client] +# Does: Starts Expo frontend and lets you choose backend target (Local or Live). +# Use: ./run-front.sh [--local|--live|--production] [--clear] [--dev-client] # DB: None (frontend only). # Ports: Metro uses LAN IP; local API defaults to :8080 (EXPO_PUBLIC_LOCAL_API_PORT overrides). # Modes: Frontend=ON | Backend=Production URL or Local URL | Live stack untouched | Dev/Test DB untouched. @@ -31,26 +31,53 @@ for arg in "$@"; do --production) MODE="production" ;; + --live) + MODE="production" + ;; *) echo "Unknown argument: $arg" - echo "Usage: ./run-front.sh [--local|--production] [--clear] [--dev-client]" + echo "Usage: ./run-front.sh [--local|--live|--production] [--clear] [--dev-client]" exit 1 ;; esac done detect_lan_ip() { - hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.' | head -n1 + local from_route="" + local iface="" + + if command -v ip >/dev/null 2>&1; then + from_route="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="src") {print $(i+1); exit}}')" + if [[ "$from_route" =~ ^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\. ]]; then + echo "$from_route" + return 0 + fi + + iface="$(ip route 2>/dev/null | awk '/^default/ {print $5; exit}')" + if [[ -n "$iface" ]]; then + ip -4 addr show dev "$iface" scope global 2>/dev/null | + awk '/inet / {print $2}' | + cut -d/ -f1 | + grep -E '^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.' | + head -n1 + return 0 + fi + fi + + hostname -I 2>/dev/null | + tr ' ' '\n' | + grep -E '^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.' | + head -n1 } LAN_IP="$(detect_lan_ip || true)" if [[ -z "$LAN_IP" ]]; then LAN_IP="127.0.0.1" - echo "Warning: Could not detect private LAN IPv4. Falling back to 127.0.0.1." + echo "Warning: Could not detect private LAN IPv4." fi if [[ -z "$MODE" ]]; then - echo "Select backend: 1) Production (devbits.ddns.net) 2) Local (LAN IP:8080)" + echo "Select backend: 1) Live (devbits.app) 2) Local (LAN IP:8080)" read -r -p "Choose [1/2]: " selection case "$selection" in 1) MODE="production" ;; @@ -69,18 +96,30 @@ else unset REACT_NATIVE_PACKAGER_HOSTNAME || true unset EXPO_PACKAGER_HOSTNAME || true fi -export EXPO_PUBLIC_API_URL="https://devbits.ddns.net" -export EXPO_PUBLIC_API_FALLBACK_URL="https://devbits.ddns.net" +export EXPO_PUBLIC_API_URL="https://devbits.app" +export EXPO_PUBLIC_API_FALLBACK_URL="https://devbits.app" if [[ "$MODE" == "local" ]]; then LOCAL_API_PORT="${EXPO_PUBLIC_LOCAL_API_PORT:-8080}" + + if [[ "$LAN_IP" == "127.0.0.1" && "${DEVBITS_ALLOW_LOOPBACK_LOCAL_API:-0}" != "1" ]]; then + echo "Error: Local mode resolved loopback (127.0.0.1), which will fail on physical devices." + echo "Fix one of the following and try again:" + echo " 1) Run without sudo so network detection can read your user network context." + echo " 2) Set EXPO_PUBLIC_LOCAL_API_URL manually, e.g. http://192.168.x.y:${LOCAL_API_PORT}." + echo " 3) If using only simulator/emulator intentionally, set DEVBITS_ALLOW_LOOPBACK_LOCAL_API=1." + exit 1 + fi + export EXPO_PUBLIC_USE_LOCAL_API=1 - export EXPO_PUBLIC_LOCAL_API_URL="http://${LAN_IP}:${LOCAL_API_PORT}" + if [[ -z "${EXPO_PUBLIC_LOCAL_API_URL:-}" ]]; then + export EXPO_PUBLIC_LOCAL_API_URL="http://${LAN_IP}:${LOCAL_API_PORT}" + fi echo "Using local backend: $EXPO_PUBLIC_LOCAL_API_URL" else export EXPO_PUBLIC_USE_LOCAL_API=0 unset EXPO_PUBLIC_LOCAL_API_URL || true - echo "Using production backend: https://devbits.ddns.net" + echo "Using live backend: https://devbits.app" fi cd "$FRONTEND_DIR" diff --git a/Bash-Scripts/sync-static.sh b/Bash-Scripts/sync-static.sh new file mode 100755 index 0000000..02df63d --- /dev/null +++ b/Bash-Scripts/sync-static.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Script: sync-static.sh +# Does: Copies compliance/static files from backend/api/static (source of truth) to frontend/public. +# Run this whenever the backend static files change to keep the frontend web build in sync. +# Use: ./sync-static.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +SRC="$ROOT/backend/api/static" +DST="$ROOT/frontend/public" + +FILES=( + apple-app-site-association + privacy-policy.html + account-deletion.html +) + +for file in "${FILES[@]}"; do + src_path="$SRC/$file" + dst_path="$DST/$file" + + if [[ ! -f "$src_path" ]]; then + echo "WARNING: Source file not found, skipping: $src_path" + continue + fi + + if cmp -s "$src_path" "$dst_path" 2>/dev/null; then + echo "Up to date: $file" + else + cp "$src_path" "$dst_path" + echo "Synced: $file" + fi +done + +echo "Sync complete." diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index dd41481..b0aa2aa 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -2,22 +2,50 @@ ## Backend -> **Live/Deployed Stack** +> **AWS EC2 (Amazon Linux) Native Deploy** > > ```bash -> cd /path/to/DevBits/backend -> docker compose up -d -> docker compose logs -f db +> # On your EC2 instance +> sudo dnf update -y +> sudo dnf install -y git tar > ``` - -> Rebuild and restart: +> +> Install Go 1.24.x (required by `backend/go.mod`): > > ```bash -> docker compose up -d --build +> curl -LO https://go.dev/dl/go1.24.2.linux-amd64.tar.gz +> sudo rm -rf /usr/local/go +> sudo tar -C /usr/local -xzf go1.24.2.linux-amd64.tar.gz +> echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc +> source ~/.bashrc +> go version > ``` - +> +> Clone and deploy: +> +> ```bash +> sudo mkdir -p /opt/devbits +> sudo chown -R "$USER":"$USER" /opt/devbits +> cd /opt/devbits +> git clone https://github.com/devbits-go/DevBits.git . +> git checkout aws-ready-main +> cd backend +> cp .env.example .env +> # edit .env with production values (DATABASE_URL, secrets, CORS, etc.) +> ./scripts/deploy-aws-native.sh +> ``` +> +> Verify service: +> +> ```bash +> sudo systemctl status devbits-api --no-pager +> sudo journalctl -u devbits-api -n 120 --no-pager +> curl -i http://127.0.0.1:8080/health +> ``` +> > [!TIP] -> Check `backend/scripts/README.md` for database operations. +> AWS deploy uses native `systemd` (no Docker or nginx required in production). +> See `backend/docs/AWS_TRANSFER_NO_NGINX.md` for full runbook. ## Build @@ -58,4 +86,4 @@ --- -`Workflow: Backend Setup → EAS Build → EAS Submit` +`Workflow: AWS Backend Deploy → EAS Build → EAS Submit` diff --git a/backend/scripts/backup-deployment-db.ps1 b/Old Scripts/backend/scripts/backup-deployment-db.ps1 similarity index 66% rename from backend/scripts/backup-deployment-db.ps1 rename to Old Scripts/backend/scripts/backup-deployment-db.ps1 index 466a097..ee60895 100644 --- a/backend/scripts/backup-deployment-db.ps1 +++ b/Old Scripts/backend/scripts/backup-deployment-db.ps1 @@ -5,12 +5,6 @@ param( $ErrorActionPreference = "Stop" -if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - $arguments = "& '" + $myinvocation.mycommand.definition + "'" - Start-Process powershell -Verb runAs -ArgumentList $arguments - exit -} - Write-Host "Creating deployment database backup..." -ForegroundColor Yellow $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path @@ -21,23 +15,28 @@ try { New-Item -ItemType Directory -Path $backupPath -Force | Out-Null $envFile = Join-Path $root ".env" - $dbUser = "devbits" - $dbName = "devbits" - if (Test-Path $envFile) { - $envRaw = Get-Content -Path $envFile -Raw - if ($envRaw -match "(?m)^POSTGRES_USER=(.+)$") { - $dbUser = $Matches[1].Trim() - } - if ($envRaw -match "(?m)^POSTGRES_DB=(.+)$") { - $dbName = $Matches[1].Trim() - } + if (-not (Test-Path $envFile)) { + throw "Missing $envFile. Create it from backend/.env.example and set DATABASE_URL." + } + + $envRaw = Get-Content -Path $envFile -Raw + $dbUrl = $null + if ($envRaw -match "(?m)^DATABASE_URL=(.+)$") { + $dbUrl = $Matches[1].Trim() + } + if ([string]::IsNullOrWhiteSpace($dbUrl)) { + throw "Missing DATABASE_URL in $envFile" + } + + if (-not (Get-Command pg_dump -ErrorAction SilentlyContinue)) { + throw "pg_dump not found in PATH. Install PostgreSQL client tools first." } $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $dbBackupFileName = "devbits-db-$timestamp.sql" $dbBackupFile = Join-Path $backupPath $dbBackupFileName - - docker compose exec -T db pg_dump -U $dbUser -d $dbName --no-owner --no-privileges | Out-File -FilePath $dbBackupFile -Encoding utf8 + + pg_dump $dbUrl --no-owner --no-privileges | Out-File -FilePath $dbBackupFile -Encoding utf8 if (-not (Test-Path $dbBackupFile)) { throw "Database backup file was not created." @@ -69,20 +68,11 @@ finally { Pop-Location } -$liveBackendState = "unavailable" -try { - $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null - if ($LASTEXITCODE -eq 0) { - $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" } - } -} -catch {} - Write-Host "" Write-Host "===== Summary =====" -ForegroundColor Cyan Write-Host "Action: Deployment backup created" Write-Host "Updated: Latest DB + uploads backup files retained" -Write-Host "Live backend: $liveBackendState" +Write-Host "Database backup target: DATABASE_URL" if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") { Read-Host "Press Enter to close" diff --git a/backend/scripts/disable-daily-backup-task.ps1 b/Old Scripts/backend/scripts/disable-daily-backup-task.ps1 similarity index 76% rename from backend/scripts/disable-daily-backup-task.ps1 rename to Old Scripts/backend/scripts/disable-daily-backup-task.ps1 index 804b456..a86467e 100644 --- a/backend/scripts/disable-daily-backup-task.ps1 +++ b/Old Scripts/backend/scripts/disable-daily-backup-task.ps1 @@ -23,20 +23,12 @@ Write-Host "Task removed." -ForegroundColor Green $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $root = Resolve-Path (Join-Path $scriptDir "..") -$liveBackendState = "unavailable" -try { - $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null - if ($LASTEXITCODE -eq 0) { - $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" } - } -} -catch {} Write-Host "" Write-Host "===== Summary =====" -ForegroundColor Cyan Write-Host "Action: Daily backup task removed" Write-Host "Updated: Windows scheduled task '$TaskName' deleted" -Write-Host "Live backend: $liveBackendState" +Write-Host "Execution target removed: backup-deployment-db.ps1" if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") { Read-Host "Press Enter to close" diff --git a/backend/scripts/reset-deployment-db.ps1 b/Old Scripts/backend/scripts/reset-deployment-db.ps1 similarity index 51% rename from backend/scripts/reset-deployment-db.ps1 rename to Old Scripts/backend/scripts/reset-deployment-db.ps1 index ee7acae..21dd784 100644 --- a/backend/scripts/reset-deployment-db.ps1 +++ b/Old Scripts/backend/scripts/reset-deployment-db.ps1 @@ -7,12 +7,6 @@ $ErrorActionPreference = "Stop" Write-Host "Resetting DevBits deployment database to a blank slate..." -ForegroundColor Yellow -if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - $arguments = "& '" + $myinvocation.mycommand.definition + "'" - Start-Process powershell -Verb runAs -ArgumentList $arguments - exit -} - $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $root = Resolve-Path (Join-Path $scriptDir "..") Push-Location $root @@ -22,7 +16,28 @@ try { throw "Missing $envFile. Create it from backend/.env.example and set strong credentials before resetting." } - docker compose down -v --remove-orphans + $envRaw = Get-Content -Path $envFile -Raw + $dbUrl = $null + if ($envRaw -match "(?m)^DATABASE_URL=(.+)$") { + $dbUrl = $Matches[1].Trim() + } + if ([string]::IsNullOrWhiteSpace($dbUrl)) { + throw "Missing DATABASE_URL in $envFile" + } + + if (-not (Get-Command psql -ErrorAction SilentlyContinue)) { + throw "psql not found in PATH. Install PostgreSQL client tools first." + } + + psql $dbUrl -v ON_ERROR_STOP=1 -c "DROP SCHEMA IF EXISTS public CASCADE;" + if ($LASTEXITCODE -ne 0) { + throw "Failed dropping public schema." + } + + psql $dbUrl -v ON_ERROR_STOP=1 -c "CREATE SCHEMA public;" + if ($LASTEXITCODE -ne 0) { + throw "Failed creating public schema." + } if (-not $KeepUploads) { $uploadsPath = Join-Path $root "uploads" @@ -31,7 +46,13 @@ try { } } - docker compose up -d --build + $hasService = (Get-Command systemctl -ErrorAction SilentlyContinue) + if ($hasService) { + systemctl list-unit-files | Select-String -Pattern '^devbits-api\.service' | Out-Null + if ($LASTEXITCODE -eq 0) { + sudo systemctl restart devbits-api + } + } Write-Host "Database reset complete. All users and app data are removed." -ForegroundColor Green } @@ -39,20 +60,11 @@ finally { Pop-Location } -$liveBackendState = "unavailable" -try { - $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null - if ($LASTEXITCODE -eq 0) { - $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" } - } -} -catch {} - Write-Host "" Write-Host "===== Summary =====" -ForegroundColor Cyan Write-Host "Action: Deployment DB reset executed" -Write-Host "Updated: Database recreated and services rebuilt" -Write-Host "Live backend: $liveBackendState" +Write-Host "Updated: Public schema recreated (blank state)" +Write-Host "Database reset target: DATABASE_URL" if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") { Read-Host "Press Enter to close" diff --git a/backend/scripts/restore-deployment-db.ps1 b/Old Scripts/backend/scripts/restore-deployment-db.ps1 similarity index 55% rename from backend/scripts/restore-deployment-db.ps1 rename to Old Scripts/backend/scripts/restore-deployment-db.ps1 index 27e8bb9..93b55ad 100644 --- a/backend/scripts/restore-deployment-db.ps1 +++ b/Old Scripts/backend/scripts/restore-deployment-db.ps1 @@ -5,25 +5,6 @@ param( $ErrorActionPreference = "Stop" -if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - $argumentList = @( - "-NoProfile", - "-ExecutionPolicy", "Bypass", - "-NoExit", - "-File", "`"$($MyInvocation.MyCommand.Path)`"" - ) - - if ($PSBoundParameters.ContainsKey("BackupDir")) { - $argumentList += @("-BackupDir", "`"$BackupDir`"") - } - if ($NoPause) { - $argumentList += "-NoPause" - } - - Start-Process powershell -Verb runAs -ArgumentList ($argumentList -join " ") - exit -} - Write-Host "Restoring deployment database from backup..." -ForegroundColor Yellow $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path @@ -51,38 +32,34 @@ try { Write-Host "Using backup: $resolvedBackup" -ForegroundColor Cyan $envFile = Join-Path $root ".env" - $dbUser = "devbits" - $dbName = "devbits" - if (Test-Path $envFile) { - $envRaw = Get-Content -Path $envFile -Raw - if ($envRaw -match "(?m)^POSTGRES_USER=(.+)$") { - $dbUser = $Matches[1].Trim() - } - if ($envRaw -match "(?m)^POSTGRES_DB=(.+)$") { - $dbName = $Matches[1].Trim() - } + if (-not (Test-Path $envFile)) { + throw "Missing $envFile. Create it from backend/.env.example and set DATABASE_URL." } - $terminateSql = "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$dbName' AND pid <> pg_backend_pid();" - $dropDbSql = "DROP DATABASE IF EXISTS `"$dbName`;" - $createDbSql = "CREATE DATABASE `"$dbName`;" + $envRaw = Get-Content -Path $envFile -Raw + $dbUrl = $null + if ($envRaw -match "(?m)^DATABASE_URL=(.+)$") { + $dbUrl = $Matches[1].Trim() + } + if ([string]::IsNullOrWhiteSpace($dbUrl)) { + throw "Missing DATABASE_URL in $envFile" + } - docker compose exec -T db psql -U $dbUser -d postgres -c $terminateSql - if ($LASTEXITCODE -ne 0) { - throw "Failed terminating active connections." + if (-not (Get-Command psql -ErrorAction SilentlyContinue)) { + throw "psql not found in PATH. Install PostgreSQL client tools first." } - docker compose exec -T db psql -U $dbUser -d postgres -c $dropDbSql + psql $dbUrl -v ON_ERROR_STOP=1 -c "DROP SCHEMA IF EXISTS public CASCADE;" if ($LASTEXITCODE -ne 0) { - throw "Failed dropping existing database." + throw "Failed dropping public schema." } - docker compose exec -T db psql -U $dbUser -d postgres -c $createDbSql + psql $dbUrl -v ON_ERROR_STOP=1 -c "CREATE SCHEMA public;" if ($LASTEXITCODE -ne 0) { - throw "Failed creating target database." + throw "Failed creating public schema." } - Get-Content -Path $resolvedBackup -Raw | docker compose exec -T db psql -U $dbUser -d $dbName + Get-Content -Path $resolvedBackup -Raw | psql $dbUrl -v ON_ERROR_STOP=1 if ($LASTEXITCODE -ne 0) { throw "Restore failed while applying SQL backup." } @@ -111,14 +88,15 @@ try { Write-Host "No matching uploads backup found for timestamp $dbTimestamp, keeping current uploads directory." -ForegroundColor Yellow } - Write-Host "Restore complete. Rebuilding deployment services..." -ForegroundColor Yellow - - docker compose up -d --build - if ($LASTEXITCODE -ne 0) { - throw "Restore completed, but service rebuild failed." + $hasService = (Get-Command systemctl -ErrorAction SilentlyContinue) + if ($hasService) { + systemctl list-unit-files | Select-String -Pattern '^devbits-api\.service' | Out-Null + if ($LASTEXITCODE -eq 0) { + sudo systemctl restart devbits-api + } } - Write-Host "Restore complete and services rebuilt." -ForegroundColor Green + Write-Host "Restore complete." -ForegroundColor Green } finally { Pop-Location @@ -130,20 +108,11 @@ catch { } finally { $operationState = if ($failed) { "failed" } else { "success" } - $liveBackendState = "unavailable" - try { - $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null - if ($LASTEXITCODE -eq 0) { - $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" } - } - } - catch {} - Write-Host "" Write-Host "===== Summary =====" -ForegroundColor Cyan Write-Host "Action: Deployment DB restore executed ($operationState)" - Write-Host "Updated: Database restored and matching uploads restored when available; services rebuilt" - Write-Host "Live backend: $liveBackendState" + Write-Host "Updated: Database restored and matching uploads restored when available" + Write-Host "Database restore target: DATABASE_URL" if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") { Read-Host "Press Enter to close" diff --git a/backend/scripts/setup-daily-backup-task.ps1 b/Old Scripts/backend/scripts/setup-daily-backup-task.ps1 similarity index 85% rename from backend/scripts/setup-daily-backup-task.ps1 rename to Old Scripts/backend/scripts/setup-daily-backup-task.ps1 index 4dfc746..63fb3ac 100644 --- a/backend/scripts/setup-daily-backup-task.ps1 +++ b/Old Scripts/backend/scripts/setup-daily-backup-task.ps1 @@ -38,20 +38,12 @@ Write-Host "Verify with: schtasks /Query /TN $TaskName /V /FO LIST" -ForegroundC $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $root = Resolve-Path (Join-Path $scriptDir "..") -$liveBackendState = "unavailable" -try { - $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null - if ($LASTEXITCODE -eq 0) { - $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" } - } -} -catch {} Write-Host "" Write-Host "===== Summary =====" -ForegroundColor Cyan Write-Host "Action: Daily backup task configured" Write-Host "Updated: Scheduled task '$TaskName' set to run at $RunAt" -Write-Host "Live backend: $liveBackendState" +Write-Host "Execution target: backup-deployment-db.ps1 (DATABASE_URL)" if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") { Read-Host "Press Enter to close" diff --git a/Old Scripts/backend/scripts/update-live.ps1 b/Old Scripts/backend/scripts/update-live.ps1 new file mode 100644 index 0000000..41ccff4 --- /dev/null +++ b/Old Scripts/backend/scripts/update-live.ps1 @@ -0,0 +1,24 @@ +param( + [Parameter(Mandatory = $true)] + [string]$ServerHost, + [string]$User = "ec2-user", + [string]$ProjectPath = "/opt/devbits/backend", + [switch]$NoPause +) + +$ErrorActionPreference = "Stop" + +Write-Host "Deploying backend on AWS host $User@$ServerHost using native Linux deploy script..." -ForegroundColor Yellow + +$remoteCommand = "cd $ProjectPath && ./scripts/deploy-aws-native.sh" +ssh "$User@$ServerHost" $remoteCommand + +if ($LASTEXITCODE -ne 0) { + throw "Remote deploy failed with exit code $LASTEXITCODE" +} + +Write-Host "Remote deploy completed successfully." -ForegroundColor Green + +if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") { + Read-Host "Press Enter to close" +} diff --git a/frontend/scripts/imagescale.ps1 b/Old Scripts/frontend/scripts/imagescale.ps1 similarity index 100% rename from frontend/scripts/imagescale.ps1 rename to Old Scripts/frontend/scripts/imagescale.ps1 diff --git a/frontend/scripts/install-adb.ps1 b/Old Scripts/frontend/scripts/install-adb.ps1 similarity index 100% rename from frontend/scripts/install-adb.ps1 rename to Old Scripts/frontend/scripts/install-adb.ps1 diff --git a/frontend/scripts/screenrecord.ps1 b/Old Scripts/frontend/scripts/screenrecord.ps1 similarity index 100% rename from frontend/scripts/screenrecord.ps1 rename to Old Scripts/frontend/scripts/screenrecord.ps1 diff --git a/Powershell-Scripts/run-db-tests.ps1 b/Old Scripts/run-db-tests.ps1 similarity index 100% rename from Powershell-Scripts/run-db-tests.ps1 rename to Old Scripts/run-db-tests.ps1 diff --git a/Powershell-Scripts/run-dev.ps1 b/Old Scripts/run-dev.ps1 similarity index 100% rename from Powershell-Scripts/run-dev.ps1 rename to Old Scripts/run-dev.ps1 diff --git a/Powershell-Scripts/run-front.ps1 b/Old Scripts/run-front.ps1 similarity index 91% rename from Powershell-Scripts/run-front.ps1 rename to Old Scripts/run-front.ps1 index 51af854..dee6dd6 100644 --- a/Powershell-Scripts/run-front.ps1 +++ b/Old Scripts/run-front.ps1 @@ -56,7 +56,7 @@ elseif ($Production) { $mode = "production" } else { - Write-Host "Select backend: 1) Production (devbits.ddns.net) 2) Local (LAN IP:8080)" + Write-Host "Select backend: 1) Production (devbits.app) 2) Local (LAN IP:8080)" $selection = Read-Host "Choose [1/2]" switch ($selection) { "1" { $mode = "production" } @@ -76,8 +76,8 @@ else { Remove-Item Env:REACT_NATIVE_PACKAGER_HOSTNAME -ErrorAction SilentlyContinue Remove-Item Env:EXPO_PACKAGER_HOSTNAME -ErrorAction SilentlyContinue } -$env:EXPO_PUBLIC_API_URL = "https://devbits.ddns.net" -$env:EXPO_PUBLIC_API_FALLBACK_URL = "https://devbits.ddns.net" +$env:EXPO_PUBLIC_API_URL = "https://devbits.app" +$env:EXPO_PUBLIC_API_FALLBACK_URL = "https://devbits.app" if ($mode -eq "local") { $port = if ($env:EXPO_PUBLIC_LOCAL_API_PORT) { $env:EXPO_PUBLIC_LOCAL_API_PORT } else { "8080" } @@ -88,7 +88,7 @@ if ($mode -eq "local") { else { $env:EXPO_PUBLIC_USE_LOCAL_API = "0" Remove-Item Env:EXPO_PUBLIC_LOCAL_API_URL -ErrorAction SilentlyContinue - Write-Host "Using production backend: https://devbits.ddns.net" -ForegroundColor Green + Write-Host "Using production backend: https://devbits.app" -ForegroundColor Green } Push-Location $frontendDir diff --git a/Powershell-Scripts/run-tests.ps1 b/Old Scripts/run-tests.ps1 similarity index 100% rename from Powershell-Scripts/run-tests.ps1 rename to Old Scripts/run-tests.ps1 diff --git a/README.md b/README.md index 44d32c8..d13cbfe 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,18 @@ Start only the frontend (choose production or local backend at launch): ./run-front.sh ``` -Start full local stack (dev PostgreSQL + backend + frontend in local API mode): +Start local backend stack only (dev PostgreSQL + backend): ```bash ./run-dev.sh ``` +Then start frontend and choose local or live backend: + +```bash +./run-front.sh +``` + Run backend tests using dockerized Go against the dev DB stack: ```bash @@ -58,7 +64,8 @@ Scan the QR code with Expo Go on your phone. The app will automatically connect ### Verification Checklist - Fresh clone frontend check: run `chmod +x run-front.sh run-dev.sh run-db-tests.sh`, then `./run-front.sh`, choose `Production`, and confirm Expo starts. -- Full local stack: run `./run-dev.sh`, confirm backend health at `http://:8080/health`, then validate app API calls from a phone on same WiFi. +- Backend stack: run `./run-dev.sh`, confirm backend health at `http://:8080/health`. +- Frontend to local backend: run `./run-front.sh --local`, then validate app API calls from a phone on same WiFi. - DB tests: run `./run-db-tests.sh` and confirm it exits with code `0`. ### Prerequisites @@ -74,8 +81,18 @@ Scan the QR code with Expo Go on your phone. The app will automatically connect For detailed instructions, see [INSTRUCTIONS.md](INSTRUCTIONS.md). +## Static file sync + +`backend/api/static/` is the source of truth for compliance and deep-linking files +(`apple-app-site-association`, `privacy-policy.html`, `account-deletion.html`). +The same files are mirrored into `frontend/public/` for the Expo web build. + +Run `Bash-Scripts/sync-static.sh` after editing any file in `backend/api/static/` +to keep the frontend copy in sync. + ## Deployment DB scripts All deployment database script commands and usage are documented in: - [backend/scripts/README.md](backend/scripts/README.md) +- [backend/docs/AWS_TRANSFER_NO_NGINX.md](backend/docs/AWS_TRANSFER_NO_NGINX.md) diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..b7acfbc --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,25 @@ +# Copy this file to backend/.env and set strong values before deployment. +# Linux/macOS: cp .env.example .env + +# Admin/API secrets (replace with strong random values) +DEVBITS_ADMIN_KEY=replace-with-strong-admin-key +DEVBITS_JWT_SECRET=replace-with-64-plus-char-random-secret + +# Preferred for AWS (RDS/Postgres) +# Replace user/password/endpoint/db name with your real RDS values. +# URL-encode special characters in password (for example ! -> %21). +DATABASE_URL=postgres://postgres:replace-with-strong-password@your-rds-endpoint:5432/postgres?sslmode=require + +# Optional local fallback values (used only when DATABASE_URL is unset) +POSTGRES_DB=devbits +POSTGRES_USER=devbits +POSTGRES_PASSWORD=replace-with-strong-password + +# Optional: comma-separated CORS origins for browser clients. +DEVBITS_CORS_ORIGINS=https://devbits.app,https://www.devbits.app + +# AWS/ALB runtime settings +DEVBITS_API_ADDR=0.0.0.0:8080 +DEVBITS_ADMIN_LOCAL_ONLY=0 +# Set true only when running behind a trusted proxy (ALB/nginx) that sets X-Forwarded-Proto. +DEVBITS_TRUST_PROXY=true diff --git a/backend/Dockerfile b/backend/Dockerfile index 363b076..c06c778 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -31,6 +31,7 @@ COPY --from=builder /app/main . # Copy the database file COPY --from=builder /app/api/internal/database/dev.sqlite3 ./api/internal/database/ COPY --from=builder /app/api/internal/database/create_tables.sql ./api/internal/database/ +COPY --from=builder /app/api/static ./api/static # Copy admin UI static files COPY --from=builder /app/api/admin ./admin diff --git a/backend/api/internal/database/dev.sqlite3-shm b/backend/api/internal/database/dev.sqlite3-shm index f70b4c0..0419e7c 100644 Binary files a/backend/api/internal/database/dev.sqlite3-shm and b/backend/api/internal/database/dev.sqlite3-shm differ diff --git a/backend/api/internal/database/dev.sqlite3-wal b/backend/api/internal/database/dev.sqlite3-wal index 3ea7a72..988ba46 100644 Binary files a/backend/api/internal/database/dev.sqlite3-wal and b/backend/api/internal/database/dev.sqlite3-wal differ diff --git a/backend/api/internal/handlers/media_ingest.go b/backend/api/internal/handlers/media_ingest.go index 2afdcfb..c9bcff8 100644 --- a/backend/api/internal/handlers/media_ingest.go +++ b/backend/api/internal/handlers/media_ingest.go @@ -86,7 +86,7 @@ func materializeMediaReference(raw string) (string, error) { if parsed.Scheme == "http" || parsed.Scheme == "https" { // Before downloading, check if the URL points to our own managed - // uploads directory (e.g. "https://devbits.ddns.net/uploads/abc.jpg"). + // uploads directory (e.g. "https://devbits.app/uploads/abc.jpg"). // If so, treat it as a local file to avoid a self-referential HTTP // request that can hang or loop. if filename, managed := extractManagedUploadFilename(parsed.Path); managed { diff --git a/backend/api/internal/handlers/media_routes.go b/backend/api/internal/handlers/media_routes.go index 3cac253..8589398 100644 --- a/backend/api/internal/handlers/media_routes.go +++ b/backend/api/internal/handlers/media_routes.go @@ -20,6 +20,11 @@ import ( const uploadDir = "uploads" +// trustProxy is evaluated once at startup to avoid repeated os.Getenv calls +// on every upload request. Set DEVBITS_TRUST_PROXY=true only when the backend +// runs behind a trusted reverse proxy (e.g. AWS ALB) that sets X-Forwarded-Proto. +var trustProxy = os.Getenv("DEVBITS_TRUST_PROXY") == "true" + var allowedImageExtensions = map[string]struct{}{ ".jpg": {}, ".jpeg": {}, @@ -209,17 +214,24 @@ func UploadMedia(context *gin.Context) { scheme := "http" if context.Request.TLS != nil { scheme = "https" + } else if trustProxy { + if forwardedProto := strings.TrimSpace(context.GetHeader("X-Forwarded-Proto")); forwardedProto != "" { + scheme = strings.ToLower(strings.TrimSpace(strings.Split(forwardedProto, ",")[0])) + if scheme != "http" && scheme != "https" { + scheme = "http" + } + } } relativeURL := fmt.Sprintf("/%s/%s", uploadDir, filename) absoluteURL := fmt.Sprintf("%s://%s%s", scheme, context.Request.Host, relativeURL) context.JSON(http.StatusOK, gin.H{ - "url": relativeURL, + "url": relativeURL, "absolute_url": absoluteURL, - "filename": filename, - "contentType": file.Header.Get("Content-Type"), - "mediaType": mediaKind, - "size": file.Size, + "filename": filename, + "contentType": file.Header.Get("Content-Type"), + "mediaType": mediaKind, + "size": file.Size, }) } diff --git a/backend/api/main.go b/backend/api/main.go index f79184a..a90cf78 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -42,7 +42,8 @@ func getAllowedOrigins() []string { } return []string{ - "https://devbits.ddns.net", + "https://devbits.app", + "https://www.devbits.app", "http://localhost:8081", "http://localhost:19006", "http://127.0.0.1:8081", @@ -65,6 +66,7 @@ func HealthCheck(context *gin.Context) { func resolveAdminDir() string { candidates := []string{ "./admin", + "./api/admin", "../admin", "../../backend/api/admin", "./backend/api/admin", @@ -193,6 +195,20 @@ func main() { } else { log.Printf("INFO: admin UI available at /admin (key-protected)") } + router.GET("/apple-app-site-association", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.File("./api/static/apple-app-site-association") + }) + router.GET("/.well-known/apple-app-site-association", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.File("./api/static/apple-app-site-association") + }) + router.GET("/.well-known/assetlinks.json", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.File("./api/static/assetlinks.json") + }) + router.StaticFile("/privacy-policy", "./api/static/privacy-policy.html") + router.StaticFile("/account-deletion", "./api/static/account-deletion.html") router.GET("/", func(c *gin.Context) { c.String(200, "Welcome to the DevBits API! Everything is running correctly.") diff --git a/backend/api/static/account-deletion.html b/backend/api/static/account-deletion.html new file mode 100644 index 0000000..8828e83 --- /dev/null +++ b/backend/api/static/account-deletion.html @@ -0,0 +1,333 @@ + + + + + + + + Become a Tester + + + + + +
+
+
+
+

Request account and data deletion — DevBits

+

+ This page explains how you can request deletion of your DevBits + account and associated data. It is provided by the developer shown on + the Google Play store listing: DevBits. +

+ +

How to request deletion

+
    +
  1. + Email us at + mail@elifouts.net with the + subject "Account Deletion Request" and include the username and the + email address associated with your account. +
  2. +
  3. + We will reply within 5 business days to confirm the request and may + ask for a brief confirmation to verify account ownership. +
  4. +
  5. + After confirmation we will process account deletion; you will + receive a confirmation email when deletion is completed. +
  6. +
+ +

What data we will delete

+
    +
  • Account record (username, profile fields, avatar).
  • +
  • + User-generated content you created: posts, comments, project pages + (these will be permanently removed). +
  • +
  • Authentication credentials (passwords) and associated tokens.
  • +
  • Push notification token(s) associated with your account.
  • +
+ +

What data may be retained

+
    +
  • + Aggregated, anonymized usage analytics (non-identifying) for product + and security monitoring. +
  • +
  • + Server logs and backups (retained for security, fraud prevention, + and legal compliance). We purge backups according to the schedule + below. +
  • +
+ +

Retention period

+

+ After you request deletion, we will remove your account and most + associated personal data within 30 days. Backups and + logs that cannot be instantly purged for technical or legal reasons + will be deleted or anonymized within 90 days unless + otherwise required by law. +

+ +

Additional notes

+
    +
  • + If your content was shared by other users (e.g. forwarded messages), + copies may remain in their accounts until those users delete them. +
  • +
  • + Deleting your account is irreversible. If you wish to stop using + DevBits temporarily, you can request account deactivation instead — + contact us and we can advise. +
  • +
+ +

+ If you need help with deletion or have legal requests, contact us at + mail@elifouts.com. +

+ +
+ Request account deletion + Privacy Policy +
+ +
+ Developer: DevBits — Last updated: February 18, 2026. Please + replace the contact email in this page with your preferred support + address before publishing. +
+
+
+ + diff --git a/backend/api/static/apple-app-site-association b/backend/api/static/apple-app-site-association new file mode 100644 index 0000000..cdc78c1 --- /dev/null +++ b/backend/api/static/apple-app-site-association @@ -0,0 +1,13 @@ +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": "RLUPQU794G.com.devbits.frontend", + "paths": [ + "*" + ] + } + ] + } +} diff --git a/backend/api/static/assetlinks.json b/backend/api/static/assetlinks.json new file mode 100644 index 0000000..a1b8fba --- /dev/null +++ b/backend/api/static/assetlinks.json @@ -0,0 +1,12 @@ +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.devbits.frontend", + "sha256_cert_fingerprints": [ + "4E:A1:DD:8D:E1:03:23:AC:35:EB:BE:B2:64:36:DF:78:17:36:B5:3E:C4:69:67:82:74:EB:21:CC:D5:0A:34:7A" + ] + } + } +] diff --git a/backend/api/static/privacy-policy.html b/backend/api/static/privacy-policy.html new file mode 100644 index 0000000..0227e39 --- /dev/null +++ b/backend/api/static/privacy-policy.html @@ -0,0 +1,491 @@ + + + + + + + + Become a Tester + + + + + +
+
+
+
+

Privacy Policy

+

Effective Date: 18/02/2026, 05:58:42

+ +

Introduction & Organizational Information

+

+ We, at Devbits ("we", "us", "our"), are committed to protecting your + privacy and to handling personal information in a responsible manner. + This policy describes how we collect, use, disclose, and protect + personal information through our website + devbits.app, our mobile + applications (Devbits on Android and iOS), and related services. +

+ +

Contact: mail@elifouts.net | Phone: +1-US 5139078569

+ +

Scope & Application

+

+ This policy applies to all visitors and users of our website and + services, including registered users and customers. It covers the + personal data we collect directly from you as well as data we collect + automatically. +

+ +

Why we process personal information

+
    +
  • + Enhance user experience by understanding needs and preferences. +
  • +
  • + Provide timely support and respond to inquiries or service requests. +
  • +
  • Operate and improve our products and services.
  • +
  • + Perform business operations such as billing and account management. +
  • +
+ +

Data we collect

+

+ We collect only the personal information necessary to provide and + improve our services. Examples include: +

+
    +
  • + Identity & contact: first and last name, email address, + username. +
  • +
  • + Device & technical: operating system and version, device model, + app version, unique identifiers (where permitted), IP address. +
  • +
  • + Content: posts, comments, images and other content you upload or + create. +
  • +
  • + Authentication: credentials and tokens used to authenticate your + account. +
  • +
  • Notifications: push tokens if you enable notifications.
  • +
+ +

How we use personal information

+
    +
  • To provide, maintain and improve our services and features.
  • +
  • To authenticate users, prevent fraud, and secure accounts.
  • +
  • To communicate with users and respond to support requests.
  • +
  • For analytics, product improvement and crash reporting.
  • +
  • + To comply with legal obligations and protect rights and safety. +
  • +
+ +

Data storage & protection

+

+ Location: Personal information is stored on secure + servers in the United States. When transfers outside your jurisdiction + are necessary, we will take steps to ensure appropriate protections + are in place. +

+

+ Security measures: We use encryption for data in + transit and at rest, access controls, and industry-standard security + practices to protect data. No system is completely secure; if a breach + occurs we will act in accordance with applicable laws. +

+ +

Data Processing Agreements

+

+ We share data with third-party service providers (hosting, backups, + analytics, email delivery, payment processors) only as necessary. + Where required by law, we enter into Data Processing Agreements (DPAs) + that oblige providers to protect personal information and meet GDPR + standards. +

+ +

Transparency & choices

+

+ We will notify you about material changes to our sharing practices and + obtain consent where required. You can manage preferences such as + notifications and certain profile fields within the App. +

+ +

User rights (GDPR & general)

+

You may exercise the following rights where applicable:

+
    +
  • Access: request a copy of personal data we hold about you.
  • +
  • Rectification: correct inaccurate or incomplete data.
  • +
  • + Erasure: request deletion of personal data ('right to be + forgotten'). +
  • +
  • Restriction: request limitation of processing.
  • +
  • Portability: receive your data in a machine-readable format.
  • +
  • Object: object to processing, including for direct marketing.
  • +
  • + Withdraw consent: withdraw consent where processing is based on it. +
  • +
+

+ To exercise these rights contact us at mail@elifouts.net or +1-US + 5139078569. We may require identity verification to process requests. +

+ +

Cookies & tracking technologies

+

+ We use cookies and similar technologies for essential functionality, + analytics, and optional advertising. A cookie banner on first visit + allows you to accept all, reject non-essential cookies, or customize + preferences. We use industry-standard providers and do not use cookies + to knowingly collect information from children under 13. +

+ +

Children's privacy

+

+ Our services are not directed at children under 13. We do not + knowingly collect personal information from children under 13 without + verifiable parental consent. If we become aware we have collected such + data, we will delete it promptly. Parents may contact us to request + deletion. +

+ +

US privacy & CCPA/CPRA

+

+ If you are a California resident you have additional rights under the + CCPA/CPRA, including the right to know what personal information we + collect, request deletion, and opt-out of certain sharing. To exercise + these rights contact mail@elifouts.net or call +1-US 5139078569. +

+ +

Children under 13 & verifiable parental consent

+

+ If a feature requires parental consent, we will obtain verifiable + parental consent using reasonable methods (e.g. signed form or other + verification). Parents may exercise rights on behalf of their children + by contacting us. +

+ +

Retention

+

+ We retain account and content data while your account exists. After + deletion requests we remove data from public view promptly and delete + backups within 90 days unless retention is required for legal reasons. +

+ +

How to request deletion

+

+ Use our Delete Account page: + /account-deletion or email + mail@elifouts.net with subject "Account Deletion Request". We will + respond and verify ownership before completing deletion. +

+ +

Changes to this policy

+

+ We may update this policy. We will publish the effective date above + and notify users for material changes. +

+ +

App Store / Play Store data collection summary

+

+ For store listings and App Privacy forms, we collect the following + categories of data: +

+
    +
  • + Identifiers: Email, username, device identifiers, + and account IDs — linked to user account. +
  • +
  • + Contact Info: Email address — linked to user + account for support and account recovery. +
  • +
  • + User Content: Posts, comments, images and uploaded + media — linked to user account. +
  • +
  • + Usage Data: App usage, analytics, crash reports, + device model, OS version — may be not linked for analytics, + but linked for debugging when necessary. +
  • +
  • + Diagnostics: Crash logs and server logs — may + contain identifiers and be linked to accounts for support. +
  • +
  • + Authentication Data: JWT auth tokens (stored + server-side and issued to clients) — used to authenticate requests; + tokens expire automatically. +
  • +
  • + Push Tokens: APNs/FCM push tokens — stored to + deliver notifications; these are linked to user accounts. +
  • +
+ +

+ You may see these categories summarized in App Store Connect when + answering the App Privacy questions. If you need a one-line export for + the form: + Identifiers, Contact Info, User Content, Usage Data, Diagnostics — + all collected; Identifiers/Contact/User Content are linked to the + user; Usage/Diagnostics are sometimes linked for debugging. +

+ +

Deep links & app links (Universal Links / App Links)

+

+ DevBits supports Universal Links (iOS) and App Links (Android). To + enable this we host two files on the server: +

+
    +
  • + Apple (AASA): + /apple-app-site-association or + /.well-known/apple-app-site-association served as JSON + with application/json content-type. It lists the app's + App ID and allowed paths. Example template is available in the repo + and must include your Apple Team ID. +
  • +
  • + Android (assetlinks.json): + /.well-known/assetlinks.json served as JSON; it must + include your Android package and SHA256 fingerprint from your upload + keystore. +
  • +
+ +

+ Hosting these files and adding the matching entitlements/associated + domains in the Apple Developer portal and Android intent filters is + required only if you want links to open directly in the native app. If + you do not require Universal Links, this step is optional but + recommended for a polished experience. +

+ +

Contact

+

+ Questions or complaints: mail@elifouts.net | +1-US 5139078569. You may + also lodge a complaint with a supervisory authority if you believe we + have not complied with applicable law. +

+ +
+ Last updated: 18/02/2026, 05:58:42 — This policy is provided for + informational purposes and does not constitute legal advice. If you + operate in regulated jurisdictions or collect data from children, + consider legal review. +
+
+ + diff --git a/backend/bin/api b/backend/bin/api new file mode 100644 index 0000000..b94c784 Binary files /dev/null and b/backend/bin/api differ diff --git a/backend/bin/devbits-api b/backend/bin/devbits-api new file mode 100644 index 0000000..5a15837 Binary files /dev/null and b/backend/bin/devbits-api differ diff --git a/backend/deploy/systemd/devbits-api.service b/backend/deploy/systemd/devbits-api.service new file mode 100644 index 0000000..17a18b0 --- /dev/null +++ b/backend/deploy/systemd/devbits-api.service @@ -0,0 +1,17 @@ +[Unit] +Description=DevBits API Service +After=network.target + +[Service] +Type=simple +User=__SERVICE_USER__ +Group=__SERVICE_USER__ +WorkingDirectory=__WORKDIR__ +EnvironmentFile=__ENV_FILE__ +ExecStart=__BINARY_PATH__ +Restart=always +RestartSec=3 +LimitNOFILE=65535 + +[Install] +WantedBy=multi-user.target diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 349c2f0..dd96c6f 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -16,8 +16,6 @@ services: interval: 5s timeout: 5s retries: 20 - networks: - - app-network backend: build: @@ -32,8 +30,6 @@ services: - "8080:8080" security_opt: - no-new-privileges:true - networks: - - app-network depends_on: db: condition: service_healthy @@ -41,31 +37,7 @@ services: - DATABASE_URL=postgres://${POSTGRES_USER:-devbits}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@db:5432/${POSTGRES_DB:-devbits}?sslmode=disable - DEVBITS_ADMIN_KEY=${DEVBITS_ADMIN_KEY} - DEVBITS_ADMIN_LOCAL_ONLY=${DEVBITS_ADMIN_LOCAL_ONLY:-0} - - nginx: - image: nginx:latest - container_name: devbits-nginx - restart: unless-stopped - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - # Mount volumes for Let's Encrypt certificates - - ./nginx/certs:/etc/letsencrypt - # Serve static site assets (public pages + screenshots) - - ../frontend/public:/usr/share/nginx/html:ro - depends_on: - backend: - condition: service_started - security_opt: - - no-new-privileges:true - networks: - - app-network - -networks: - app-network: - driver: bridge + - DEVBITS_CORS_ORIGINS=${DEVBITS_CORS_ORIGINS:-https://devbits.app,https://www.devbits.app} volumes: postgres-data: diff --git a/backend/docs/AWS_TRANSFER_NO_NGINX.md b/backend/docs/AWS_TRANSFER_NO_NGINX.md new file mode 100644 index 0000000..db3a01f --- /dev/null +++ b/backend/docs/AWS_TRANSFER_NO_NGINX.md @@ -0,0 +1,90 @@ +# DevBits AWS Transfer (Native Backend, No Docker, No Nginx) + +This runbook deploys the Go backend as a native Linux service on EC2. + +## Target architecture + +- Route 53 (`devbits.app`, optional `www.devbits.app`) +- ACM certificate +- Application Load Balancer (HTTPS 443) +- EC2 instance running `devbits-api` via `systemd` +- PostgreSQL on AWS RDS (recommended) or another managed Postgres + +## What changed in this branch + +- `devbits.ddns.net` defaults moved to `devbits.app`. +- nginx runtime dependency removed from deployment path. +- Backend serves: + - `/apple-app-site-association` + - `/.well-known/assetlinks.json` + - `/privacy-policy` + - `/account-deletion` +- Upload absolute URLs now honor `X-Forwarded-Proto` (correct behind ALB). +- Added native deploy scripts: + - `backend/scripts/build-backend-linux.sh` + - `backend/scripts/install-aws-systemd-service.sh` + - `backend/scripts/deploy-aws-native.sh` + - `backend/scripts/update-live.sh` (wrapper) + +## What you give the AWS account owner + +1. Repo URL + branch name. +2. Domain: `devbits.app` (+ optional `www.devbits.app`). +3. Region (example: `us-east-1`). +4. These env values for `backend/.env`: + - `DATABASE_URL=postgres://...` (RDS endpoint, db, user, password, sslmode=require) + - `DEVBITS_JWT_SECRET` + - `DEVBITS_ADMIN_KEY` + - `DEVBITS_ADMIN_LOCAL_ONLY=0` (or `1` for localhost-only admin) + - `DEVBITS_CORS_ORIGINS=https://devbits.app,https://www.devbits.app` + - `DEVBITS_API_ADDR=0.0.0.0:8080` +5. Optional data migration files: + - DB dump (`devbits-db-*.sql`) + - uploads archive + +## AWS setup steps (admin) + +1. Create Route 53 records for `devbits.app` and `www.devbits.app`. +2. Request ACM cert for both names. +3. Create ALB: + - Listener `80` -> redirect to `443` + - Listener `443` -> target group on EC2 `:8080` + - Health check path `/health` +4. Create EC2 (Amazon Linux 2023 recommended). +5. Security groups: + - ALB SG: inbound `80/443` from internet + - EC2 SG: inbound `8080` from ALB SG only, `22` from admin IP only +6. Provision EC2: + - Install `git` and `tar`: `sudo dnf install -y git tar` + - Install Go `1.24.x` from `go.dev` tarball to `/usr/local/go` + - Clone repo to `/opt/devbits` (typically as `ec2-user`) + - `cd /opt/devbits/backend` + - `cp .env.example .env` and fill real values + - `./scripts/deploy-aws-native.sh` +7. Verify: + - `https://devbits.app/health` + - `https://devbits.app/privacy-policy` + - `https://devbits.app/account-deletion` + - `https://devbits.app/apple-app-site-association` + - `https://devbits.app/.well-known/assetlinks.json` + +## Updating backend after code changes + +On EC2: + +```bash +cd /opt/devbits +git pull origin +cd backend +./scripts/update-live.sh +``` + +Amazon Linux notes: + +- Default SSH user is usually `ec2-user`. +- Use `dnf` (not `apt`) for packages. +- Keep `/opt/devbits` owned by the deploy user so build/deploy scripts can run without permission issues. + +## Notes + +- Docker is still available in this repo for local workflows, but AWS deployment in this runbook is native (`systemd`) and does not require Docker or nginx. diff --git a/backend/go.mod b/backend/go.mod index af52653..ff6845e 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -7,6 +7,7 @@ require ( github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/gorilla/websocket v1.5.3 + github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.11.2 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 @@ -28,7 +29,6 @@ require ( github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/kr/text v0.2.0 // indirect diff --git a/backend/nginx/nginx.conf b/backend/nginx/nginx.conf deleted file mode 100644 index 28f1161..0000000 --- a/backend/nginx/nginx.conf +++ /dev/null @@ -1,579 +0,0 @@ -worker_processes 1; - -events { - worker_connections 1024; -} - -http { - upstream backend_upstream { - server backend:8080; - keepalive 64; - } - - map $http_upgrade $connection_upgrade { - default upgrade; - '' close; - } - - map "$request_method $request_uri" $loggable { - default 1; - "GET /favicon.ico" 0; - "GET /apple-touch-icon.png" 0; - "GET /apple-touch-icon-precomposed.png" 0; - "POST /" 0; - } - - map $request_uri $honeypot_hit { - default 0; - ~*^/(wp-admin|wp-login\.php|xmlrpc\.php|phpmyadmin|pma|\.git|\.env|cgi-bin|boaform|actuator|manager/html|server-status|vendor/phpunit|\.well-known/pki-validation) 1; - } - - map $honeypot_hit $honeypot_loggable { - default 0; - 1 1; - } - - access_log /dev/stdout combined if=$loggable; - access_log /dev/stdout combined if=$honeypot_loggable; - error_log /dev/stderr warn; - - limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=10r/m; - limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/s; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 30; - keepalive_requests 1000; - types_hash_max_size 2048; - - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_comp_level 6; - gzip_types - text/plain - text/css - text/xml - application/xml - application/json - application/javascript - image/svg+xml; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - client_max_body_size 64m; - proxy_set_header Authorization $http_authorization; - server_tokens off; - - # HTTP server for local development and health checks - server { - listen 80 default_server; - server_name _; - add_header X-Content-Type-Options "nosniff" always; - add_header X-Frame-Options "DENY" always; - add_header Referrer-Policy "same-origin" always; - - location ~* ^/(wp-admin|wp-login\.php|xmlrpc\.php|phpmyadmin|pma|\.git|\.env|cgi-bin|boaform|actuator|manager/html|server-status|vendor/phpunit|\.well-known/pki-validation) { - return 444; - } - - location ~* ^/(media/upload|users/.+/(profile-picture|update))$ { - client_max_body_size 64m; - client_body_timeout 30s; - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - proxy_request_buffering on; - proxy_buffering on; - proxy_read_timeout 30s; - proxy_send_timeout 30s; - add_header Cache-Control "no-store" always; - } - - location ~* ^/users/[^/]+$ { - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - proxy_request_buffering on; - proxy_buffering on; - proxy_read_timeout 30s; - proxy_send_timeout 30s; - add_header Cache-Control "no-store" always; - } - - location ~ ^/(auth|feed|users|projects|posts|comments|notifications|messages|media|health)(/|$) { - limit_req zone=api_limit burst=120 nodelay; - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - location ^~ /uploads/ { - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - add_header Cache-Control "no-cache, max-age=0, must-revalidate" always; - } - - location ~* \.(png|jpg|jpeg|gif|webp|svg|ico)$ { - root /usr/share/nginx/html; - try_files $uri =404; - add_header Cache-Control "public, max-age=86400" always; - } - - location = /Cards.svg { - default_type image/svg+xml; - alias /usr/share/nginx/html/Cards.svg; - add_header Cache-Control "public, max-age=86400" always; - } - - location = /cards.svg { - default_type image/svg+xml; - alias /usr/share/nginx/html/Cards.svg; - add_header Cache-Control "public, max-age=86400" always; - } - - location / { - root /usr/share/nginx/html; - try_files $uri $uri/ @backend; - } - - location @backend { - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - # Serve Apple App Site Association for Universal Links - location = /apple-app-site-association { - default_type application/json; - alias /usr/share/nginx/html/apple-app-site-association; - } - - # Serve Android assetlinks (App Links) - location = /.well-known/assetlinks.json { - default_type application/json; - alias /usr/share/nginx/html/.well-known/assetlinks.json; - } - - location /api/ { - limit_req zone=api_limit burst=120 nodelay; - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - location = /admin { - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - location ^~ /admin/ { - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - location = /api/auth/login { - limit_req zone=auth_limit burst=40 nodelay; - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - location = /api/auth/register { - limit_req zone=auth_limit burst=40 nodelay; - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - } - - # Redirect all HTTP traffic to HTTPS for production domain - server { - listen 80; - server_name devbits.ddns.net; # <-- IMPORTANT: Replace with your DDNS domain - return 301 https://$host$request_uri; - } - - # HTTPS server - server { - listen 443 ssl http2; - server_name devbits.ddns.net; # <-- IMPORTANT: Replace with your DDNS domain - add_header X-Content-Type-Options "nosniff" always; - add_header X-Frame-Options "DENY" always; - add_header Referrer-Policy "same-origin" always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - - location ~* ^/(wp-admin|wp-login\.php|xmlrpc\.php|phpmyadmin|pma|\.git|\.env|cgi-bin|boaform|actuator|manager/html|server-status|vendor/phpunit|\.well-known/pki-validation) { - return 444; - } - - # SSL Certificate - ssl_certificate /etc/letsencrypt/live/devbits.ddns.net/fullchain.pem; # <-- IMPORTANT: Replace with your domain - ssl_certificate_key /etc/letsencrypt/live/devbits.ddns.net/privkey.pem; # <-- IMPORTANT: Replace with your domain - - # Improve SSL security - ssl_protocols TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers on; - ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; - - # Session / ticket settings - ssl_session_cache shared:SSL:50m; - ssl_session_timeout 1d; - ssl_session_tickets on; - - # OCSP stapling disabled because current certificate chain has no responder URL. - ssl_stapling off; - ssl_stapling_verify off; - resolver 1.1.1.1 8.8.8.8 8.8.4.4 ipv6=off valid=300s; - resolver_timeout 5s; - ssl_trusted_certificate /etc/letsencrypt/live/devbits.ddns.net/chain.pem; - - # Disable gzip on TLS endpoint to mitigate BREACH risk for sensitive responses - gzip off; - - location ~* ^/(media/upload|users/.+/(profile-picture|update))$ { - client_max_body_size 64m; - client_body_timeout 180s; - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - proxy_request_buffering on; - proxy_buffering on; - proxy_read_timeout 180s; - proxy_send_timeout 180s; - add_header Cache-Control "no-store" always; - } - - location ~* ^/users/[^/]+$ { - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - proxy_request_buffering on; - proxy_buffering on; - proxy_read_timeout 180s; - proxy_send_timeout 180s; - add_header Cache-Control "no-store" always; - } - - location ~ ^/(auth|feed|users|projects|posts|comments|notifications|messages|media|health)(/|$) { - limit_req zone=api_limit burst=120 nodelay; - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - location ^~ /uploads/ { - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - add_header Cache-Control "no-cache, max-age=0, must-revalidate" always; - } - - location ~* \.(png|jpg|jpeg|gif|webp|svg|ico)$ { - root /usr/share/nginx/html; - try_files $uri =404; - add_header Cache-Control "public, max-age=86400" always; - } - - location = /Cards.svg { - default_type image/svg+xml; - alias /usr/share/nginx/html/Cards.svg; - add_header Cache-Control "public, max-age=86400" always; - } - - location = /cards.svg { - default_type image/svg+xml; - alias /usr/share/nginx/html/Cards.svg; - add_header Cache-Control "public, max-age=86400" always; - } - - location / { - root /usr/share/nginx/html; - try_files $uri $uri/ @backend; - } - - # Serve Apple App Site Association for Universal Links - location = /apple-app-site-association { - default_type application/json; - alias /usr/share/nginx/html/apple-app-site-association; - } - - # Serve Android assetlinks (App Links) - location = /.well-known/assetlinks.json { - default_type application/json; - alias /usr/share/nginx/html/.well-known/assetlinks.json; - } - - location @backend { - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - # Serve static pages directly from nginx - location = /account-deletion { - default_type text/html; - alias /usr/share/nginx/html/account-deletion.html; - } - - location = /account-deletion.html { - default_type text/html; - alias /usr/share/nginx/html/account-deletion.html; - } - - # Serve the Privacy Policy page required by store listings - location = /privacy-policy { - default_type text/html; - alias /usr/share/nginx/html/privacy-policy.html; - } - - location = /privacy-policy.html { - default_type text/html; - alias /usr/share/nginx/html/privacy-policy.html; - } - - location = /api/auth/login { - limit_req zone=auth_limit burst=20 nodelay; - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - location = /api/auth/register { - limit_req zone=auth_limit burst=20 nodelay; - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - location /api/ { - limit_req zone=api_limit burst=120 nodelay; - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - location = /admin { - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - location ^~ /admin/ { - proxy_pass http://backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache off; - proxy_cache_bypass 1; - proxy_no_cache 1; - add_header Cache-Control "no-store" always; - } - - # Serve the CSAE (Child Sexual Abuse & Exploitation) standards page - location = /csae-standards { - default_type text/html; - alias /usr/share/nginx/html/csae-standards.html; - } - - location = /csae-standards.html { - default_type text/html; - alias /usr/share/nginx/html/csae-standards.html; - } - - # Serve the tester application page - location = /tester-application { - default_type text/html; - alias /usr/share/nginx/html/tester-application.html; - } - - location = /tester-application.html { - default_type text/html; - alias /usr/share/nginx/html/tester-application.html; - } - - # Serve features page - location = /features { - default_type text/html; - alias /usr/share/nginx/html/features.html; - } - - location = /features.html { - default_type text/html; - alias /usr/share/nginx/html/features.html; - } - - # Serve about page - location = /about { - default_type text/html; - alias /usr/share/nginx/html/about.html; - } - - location = /about.html { - default_type text/html; - alias /usr/share/nginx/html/about.html; - } - - } -} diff --git a/backend/scripts/README.md b/backend/scripts/README.md index 829b448..7748d6c 100644 --- a/backend/scripts/README.md +++ b/backend/scripts/README.md @@ -1,165 +1,118 @@ -# DevBits Database Scripts +# DevBits Backend Scripts -All deployment database scripts are in this folder. +Run scripts from `backend/`. -## Environment separation (important) +## 1) Update backend on AWS -### Local DB (development machine) - -Run from project root: - -```powershell -cd c:\Users\eligf\DevBits -``` - -Use compose file path explicitly: - -```powershell -docker compose -f backend/docker-compose.yml up -d -docker compose -f backend/docker-compose.yml logs -f db -``` - -### Live DB (deployed server) - -Run on server in backend directory: +Build locally, then copy binary to EC2 (recommended): ```bash -cd /path/to/DevBits/backend -docker compose up -d -docker compose logs -f db +# local machine +cd /home/ws-73/OldFiles/projects/DevBits/backend +TARGET_GOOS=linux TARGET_GOARCH=amd64 ./scripts/build-backend-linux.sh +scp -i ./bin/devbits-api ec2-user@:/tmp/devbits-api ``` -Only run reset/restore in the environment you mean to modify. - -## Script location +On the EC2 host: -Run script commands from `backend`: - -```powershell +```bash +cd /opt/devbits +# Save any local EC2 edits before pulling +git stash push -u -m "ec2-local-before-pull-$(date +%Y%m%d-%H%M%S)" +git pull origin aws-ready-main cd backend +sudo mv /tmp/devbits-api ./bin/devbits-api +sudo chown ec2-user:ec2-user ./bin/devbits-api +sudo chmod +x ./bin/devbits-api +sudo ./scripts/install-aws-systemd-service.sh ``` -## Required env file +If you intentionally want to discard local EC2 changes instead: -Before running deploy/reset/update scripts, ensure `backend/.env` exists: - -```powershell -Copy-Item .env.example .env +```bash +cd /opt/devbits +git reset --hard +git clean -fd +git pull origin aws-ready-main +cd backend +./scripts/deploy-aws-native.sh ``` -Set a strong `POSTGRES_PASSWORD` value in `.env`. - -## Scripts - -- `scripts/reset-deployment-db.ps1` / `scripts/reset-deployment-db.sh` -- `scripts/backup-deployment-db.ps1` / `scripts/backup-deployment-db.sh` -- `scripts/restore-deployment-db.ps1` / `scripts/restore-deployment-db.sh` -- `scripts/setup-daily-backup-task.ps1` -- `scripts/disable-daily-backup-task.ps1` +Verify: -## 1) Reset DB (blank slate) - -Warning: this wipes all app data in that environment. - -PowerShell: - -```powershell -./scripts/reset-deployment-db.ps1 +```bash +sudo systemctl status devbits-api --no-pager +curl -i http://127.0.0.1:8080/health ``` -Keep uploads while resetting only DB volume: - -```powershell -./scripts/reset-deployment-db.ps1 -KeepUploads -``` +## 1.1) Extra AWS checks -Bash: +Run on EC2: ```bash -./scripts/reset-deployment-db.sh -./scripts/reset-deployment-db.sh --keep-uploads -``` - -## 2) Backup DB (single-backup retention) +# Service state +sudo systemctl is-active devbits-api +sudo systemctl status devbits-api --no-pager -Safe for both local and live. Run it in the target environment. +# Process is listening on 8080 +sudo ss -ltnp | grep ':8080' -PowerShell: +# Recent service logs +sudo journalctl -u devbits-api -n 150 --no-pager -```powershell -./scripts/backup-deployment-db.ps1 +# Follow logs live while testing app traffic +sudo journalctl -u devbits-api -f ``` -Bash: +Database connectivity check (from EC2): ```bash -./scripts/backup-deployment-db.sh -``` - -Backup location: - -- `backend/backups/db` - -Retention policy: - -- keeps only the newest `devbits-*.sql` -- deletes older backup files automatically - -Backup type: - -- Logical SQL dump created with `pg_dump` from the running DB container -- Not a Docker volume snapshot/image snapshot - -## 3) Restore DB from latest backup +# Ensure PostgreSQL client tools are installed +sudo dnf install -y postgresql15 -Warning: restore terminates sessions and recreates DB in that environment. - -PowerShell: - -```powershell -./scripts/restore-deployment-db.ps1 +# Uses DATABASE_URL from backend/.env +cd /opt/devbits/backend +set -a; . ./.env; set +a +psql "$DATABASE_URL" -c "select current_user, current_database();" ``` -Bash: +DNS/public checks (from local machine or EC2): ```bash -./scripts/restore-deployment-db.sh +dig +short devbits.app +curl -i https://devbits.app/health +curl -i https://devbits.app/privacy-policy +curl -i https://devbits.app/.well-known/assetlinks.json +curl -i https://devbits.app/apple-app-site-association ``` -Restore behavior: - -- picks latest backup file from `backend/backups/db` -- terminates active DB sessions -- drops and recreates `devbits` -- applies SQL dump - -## 4) Enable daily auto backup (Windows) - -Create a scheduled task at 03:00 daily: +Target group health check path should return 200: -```powershell -./scripts/setup-daily-backup-task.ps1 +```bash +curl -i http://127.0.0.1:8080/health ``` -Custom time: +## 2) Script usage -```powershell -./scripts/setup-daily-backup-task.ps1 -RunAt "01:30" -``` +Deploy/build: -Notes: +- `scripts/build-backend-linux.sh` + - Build backend binary to `bin/devbits-api`. +- `scripts/install-aws-systemd-service.sh` + - Install/restart `devbits-api` systemd service. +- `scripts/deploy-aws-native.sh` + - Build + install/restart in one command. +- `scripts/update-live.sh` + - Wrapper for `deploy-aws-native.sh`. -- Script tries `SYSTEM` first. -- If shell is not elevated, it falls back to current-user mode. +Database scripts (use `DATABASE_URL` in `backend/.env`): -Verify task: +- `scripts/reset-deployment-db.sh` +- `--keep-uploads` to keep uploads. +- `scripts/backup-deployment-db.sh` +- `scripts/restore-deployment-db.sh` +- restores latest `devbits-db-*.sql` and matching uploads archive if present. -```powershell -schtasks /Query /TN DevBitsDailyDbBackup /V /FO LIST -``` +Required tools for DB scripts: -## 5) Disable daily auto backup (Windows) - -```powershell -./scripts/disable-daily-backup-task.ps1 -``` +- Linux: `postgresql` client package (`psql`, `pg_dump`) diff --git a/backend/scripts/backup-deployment-db.sh b/backend/scripts/backup-deployment-db.sh index 2463bed..ce10b0d 100644 --- a/backend/scripts/backup-deployment-db.sh +++ b/backend/scripts/backup-deployment-db.sh @@ -1,28 +1,32 @@ #!/usr/bin/env bash set -euo pipefail -if [ "$EUID" -ne 0 ]; then - echo "Please run as root" - sudo "$0" "$@" - exit -fi +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$ROOT_DIR" BACKUP_DIR="${1:-backups/db}" echo "Creating deployment database backup..." -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -cd "$ROOT_DIR" +if [[ ! -f ".env" ]]; then + echo "Missing $ROOT_DIR/.env. Create it from backend/.env.example and set DATABASE_URL." >&2 + exit 1 +fi + +set -a +. ./.env +set +a -if [[ -f ".env" ]]; then - set -a - . ./.env - set +a +if [[ -z "${DATABASE_URL:-}" ]]; then + echo "Missing DATABASE_URL in $ROOT_DIR/.env" >&2 + exit 1 fi -DB_USER="${POSTGRES_USER:-devbits}" -DB_NAME="${POSTGRES_DB:-devbits}" +if ! command -v pg_dump >/dev/null 2>&1; then + echo "pg_dump is required. Install PostgreSQL client tools first." >&2 + exit 1 +fi mkdir -p "$BACKUP_DIR" @@ -30,42 +34,34 @@ TIMESTAMP="$(date +"%Y%m%d-%H%M%S")" DB_BACKUP_FILE_NAME="devbits-db-${TIMESTAMP}.sql" DB_OUTPUT_FILE="${BACKUP_DIR}/${DB_BACKUP_FILE_NAME}" -docker compose exec -T db pg_dump -U "$DB_USER" -d "$DB_NAME" --no-owner --no-privileges > "$DB_OUTPUT_FILE" +pg_dump "$DATABASE_URL" --no-owner --no-privileges > "$DB_OUTPUT_FILE" if [[ ! -s "$DB_OUTPUT_FILE" ]]; then echo "Database backup file is empty or missing. Aborting." >&2 exit 1 fi + echo "Database backup created: $DB_OUTPUT_FILE" UPLOADS_DIR="uploads" UPLOADS_BACKUP_FILE_NAME="devbits-uploads-${TIMESTAMP}.tar.gz" UPLOADS_OUTPUT_FILE="${BACKUP_DIR}/${UPLOADS_BACKUP_FILE_NAME}" -if [ -d "$UPLOADS_DIR" ]; then +if [[ -d "$UPLOADS_DIR" ]]; then tar -czf "$UPLOADS_OUTPUT_FILE" -C "$UPLOADS_DIR" . echo "Uploads backup created: $UPLOADS_OUTPUT_FILE" else echo "Uploads directory not found, skipping backup." fi -# Retention: keep only the latest db backup and the latest uploads backup +# Retention: keep only the latest DB backup and latest uploads backup. find "$BACKUP_DIR" -maxdepth 1 -type f -name 'devbits-db-*.sql' | sort | head -n -1 | xargs -r rm find "$BACKUP_DIR" -maxdepth 1 -type f -name 'devbits-uploads-*.tar.gz' | sort | head -n -1 | xargs -r rm echo "Retention applied: only latest backup of each type is kept." -live_backend_state="unavailable" -if backend_status_output="$(docker compose ps backend 2>/dev/null)"; then - if echo "$backend_status_output" | grep -q "Up"; then - live_backend_state="running" - else - live_backend_state="not running" - fi -fi - echo echo "===== Summary =====" echo "Action: Deployment backup created" echo "Updated: Latest DB + uploads backup files retained" -echo "Live backend: $live_backend_state" +echo "Database backup target: DATABASE_URL" diff --git a/backend/scripts/build-backend-linux.sh b/backend/scripts/build-backend-linux.sh new file mode 100644 index 0000000..95883f8 --- /dev/null +++ b/backend/scripts/build-backend-linux.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$ROOT_DIR" + +mkdir -p bin uploads + +TARGET_GOOS="${TARGET_GOOS:-linux}" +if [[ -n "${TARGET_GOARCH:-}" ]]; then + TARGET_GOARCH="${TARGET_GOARCH}" +else + case "$(uname -m)" in + x86_64) TARGET_GOARCH="amd64" ;; + aarch64|arm64) TARGET_GOARCH="arm64" ;; + *) TARGET_GOARCH="amd64" ;; + esac +fi +OUTPUT_PATH="${OUTPUT_PATH:-$ROOT_DIR/bin/devbits-api}" + +echo "Building DevBits backend for ${TARGET_GOOS}/${TARGET_GOARCH}..." +CGO_ENABLED=0 GOOS="$TARGET_GOOS" GOARCH="$TARGET_GOARCH" go build -trimpath -ldflags="-s -w" -o "$OUTPUT_PATH" ./api + +echo "Build complete: $OUTPUT_PATH" diff --git a/backend/scripts/deploy-aws-native.sh b/backend/scripts/deploy-aws-native.sh new file mode 100644 index 0000000..a15eb8f --- /dev/null +++ b/backend/scripts/deploy-aws-native.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$ROOT_DIR" + +if [[ ! -f ".env" ]]; then + echo "Missing $ROOT_DIR/.env. Create it from backend/.env.example and set real values." >&2 + exit 1 +fi + +echo "Building backend binary..." +"$ROOT_DIR/scripts/build-backend-linux.sh" + +if [[ "$EUID" -eq 0 ]]; then + "$ROOT_DIR/scripts/install-aws-systemd-service.sh" +else + sudo "$ROOT_DIR/scripts/install-aws-systemd-service.sh" +fi + +echo +echo "===== Summary =====" +echo "Action: Native AWS deploy completed" +echo "Binary: $ROOT_DIR/bin/devbits-api" +echo "Service: ${DEVBITS_SERVICE_NAME:-devbits-api}" diff --git a/backend/scripts/install-aws-systemd-service.sh b/backend/scripts/install-aws-systemd-service.sh new file mode 100644 index 0000000..e666325 --- /dev/null +++ b/backend/scripts/install-aws-systemd-service.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$EUID" -ne 0 ]]; then + echo "Please run as root" + sudo "$0" "$@" + exit +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +SERVICE_NAME="${DEVBITS_SERVICE_NAME:-devbits-api}" +SERVICE_USER="${DEVBITS_SERVICE_USER:-${SUDO_USER:-ec2-user}}" +WORK_DIR="${DEVBITS_WORKDIR:-$ROOT_DIR}" +ENV_FILE="${DEVBITS_ENV_FILE:-$WORK_DIR/.env}" +BINARY_PATH="${DEVBITS_BINARY_PATH:-$WORK_DIR/bin/devbits-api}" +TEMPLATE_PATH="$ROOT_DIR/deploy/systemd/devbits-api.service" +SERVICE_PATH="/etc/systemd/system/${SERVICE_NAME}.service" + +if [[ ! -f "$ENV_FILE" ]]; then + echo "Missing env file: $ENV_FILE" >&2 + echo "Create it from backend/.env.example before installing service." >&2 + exit 1 +fi + +if [[ ! -x "$BINARY_PATH" ]]; then + echo "Missing executable binary: $BINARY_PATH" >&2 + echo "Run ./scripts/build-backend-linux.sh first." >&2 + exit 1 +fi + +if [[ ! -f "$TEMPLATE_PATH" ]]; then + echo "Missing systemd service template: $TEMPLATE_PATH" >&2 + echo "Ensure the deploy/systemd/devbits-api.service template is present." >&2 + exit 1 +fi + +if ! id "$SERVICE_USER" >/dev/null 2>&1; then + echo "Service user does not exist: $SERVICE_USER" >&2 + exit 1 +fi + +tmp_service="$(mktemp)" +trap 'rm -f "$tmp_service"' EXIT + +sed \ + -e "s|__SERVICE_USER__|$SERVICE_USER|g" \ + -e "s|__WORKDIR__|$WORK_DIR|g" \ + -e "s|__ENV_FILE__|$ENV_FILE|g" \ + -e "s|__BINARY_PATH__|$BINARY_PATH|g" \ + "$TEMPLATE_PATH" > "$tmp_service" + +install -m 0644 "$tmp_service" "$SERVICE_PATH" + +systemctl daemon-reload +systemctl enable "$SERVICE_NAME" +systemctl restart "$SERVICE_NAME" + +echo "Service installed: $SERVICE_PATH" +systemctl --no-pager --full status "$SERVICE_NAME" | sed -n '1,25p' diff --git a/backend/scripts/reset-deployment-db.sh b/backend/scripts/reset-deployment-db.sh index 868dbfe..db7c2b8 100644 --- a/backend/scripts/reset-deployment-db.sh +++ b/backend/scripts/reset-deployment-db.sh @@ -3,12 +3,6 @@ set -euo pipefail KEEP_UPLOADS="${1:-}" -if [ "$EUID" -ne 0 ]; then - echo "Please run as root" - sudo "$0" "$@" - exit -fi - echo "Resetting DevBits deployment database to a blank slate..." SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -20,7 +14,22 @@ if [[ ! -f ".env" ]]; then exit 1 fi -docker compose down -v --remove-orphans +set -a +. ./.env +set +a + +if [[ -z "${DATABASE_URL:-}" ]]; then + echo "Missing DATABASE_URL in $ROOT_DIR/.env" >&2 + exit 1 +fi + +if ! command -v psql >/dev/null 2>&1; then + echo "psql is required. Install PostgreSQL client tools first." >&2 + exit 1 +fi + +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "DROP SCHEMA IF EXISTS public CASCADE;" +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "CREATE SCHEMA public;" if [[ "$KEEP_UPLOADS" != "--keep-uploads" ]]; then if [[ -d "uploads" ]]; then @@ -28,21 +37,15 @@ if [[ "$KEEP_UPLOADS" != "--keep-uploads" ]]; then fi fi -docker compose up -d --build +if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^devbits-api\.service'; then + echo "Restarting devbits-api service to recreate schema and warm startup..." + sudo systemctl restart devbits-api +fi echo "Database reset complete. All users and app data are removed." -live_backend_state="unavailable" -if backend_status_output="$(docker compose ps backend 2>/dev/null)"; then - if echo "$backend_status_output" | grep -q "Up"; then - live_backend_state="running" - else - live_backend_state="not running" - fi -fi - echo echo "===== Summary =====" echo "Action: Deployment DB reset executed" -echo "Updated: Database recreated and services rebuilt" -echo "Live backend: $live_backend_state" +echo "Updated: Public schema recreated (blank state)" +echo "Database reset target: DATABASE_URL" diff --git a/backend/scripts/restore-deployment-db.sh b/backend/scripts/restore-deployment-db.sh index d1e304b..85c4190 100644 --- a/backend/scripts/restore-deployment-db.sh +++ b/backend/scripts/restore-deployment-db.sh @@ -9,19 +9,23 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$ROOT_DIR" -if [[ -f ".env" ]]; then - set -a - . ./.env - set +a +if [[ ! -f ".env" ]]; then + echo "Missing $ROOT_DIR/.env. Create it from backend/.env.example and set real values." >&2 + exit 1 fi -DB_USER="${POSTGRES_USER:-devbits}" -DB_NAME="${POSTGRES_DB:-devbits}" +set -a +. ./.env +set +a + +if [[ -z "${DATABASE_URL:-}" ]]; then + echo "Missing DATABASE_URL in $ROOT_DIR/.env" >&2 + exit 1 +fi -if [ "$EUID" -ne 0 ]; then - echo "Please run as root" - sudo "$0" "$@" - exit +if ! command -v psql >/dev/null 2>&1; then + echo "psql is required. Install PostgreSQL client tools first." >&2 + exit 1 fi if [[ ! -d "$BACKUP_DIR" ]]; then @@ -39,10 +43,9 @@ RESOLVED_BACKUP="$LATEST_FILE" echo "Using backup: $RESOLVED_BACKUP" -docker compose exec -T db psql -U "$DB_USER" -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${DB_NAME}' AND pid <> pg_backend_pid();" -docker compose exec -T db psql -U "$DB_USER" -d postgres -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" -docker compose exec -T db psql -U "$DB_USER" -d postgres -c "CREATE DATABASE \"${DB_NAME}\";" -docker compose exec -T db psql -U "$DB_USER" -d "$DB_NAME" < "$RESOLVED_BACKUP" +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "DROP SCHEMA IF EXISTS public CASCADE;" +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "CREATE SCHEMA public;" +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 < "$RESOLVED_BACKUP" db_file_name="$(basename "$RESOLVED_BACKUP")" db_timestamp="${db_file_name#devbits-db-}" @@ -65,22 +68,15 @@ else echo "No matching uploads backup found for timestamp $db_timestamp, keeping current uploads directory." fi -echo "Restore complete. Rebuilding deployment services..." -docker compose up -d --build - -echo "Restore complete and services rebuilt." - -live_backend_state="unavailable" -if backend_status_output="$(docker compose ps backend 2>/dev/null)"; then - if echo "$backend_status_output" | grep -q "Up"; then - live_backend_state="running" - else - live_backend_state="not running" - fi +if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^devbits-api\.service'; then + echo "Restarting devbits-api service..." + sudo systemctl restart devbits-api fi +echo "Restore complete." + echo echo "===== Summary =====" echo "Action: Deployment DB restore executed" -echo "Updated: Database restored and matching uploads restored when available; services rebuilt" -echo "Live backend: $live_backend_state" +echo "Updated: Database restored and matching uploads restored when available" +echo "Database restore target: DATABASE_URL" diff --git a/backend/scripts/update-live.ps1 b/backend/scripts/update-live.ps1 deleted file mode 100644 index d9056ca..0000000 --- a/backend/scripts/update-live.ps1 +++ /dev/null @@ -1,108 +0,0 @@ -param( - [switch]$NoPause -) - -if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - $arguments = "& '" + $myinvocation.mycommand.definition + "'" - Start-Process powershell -Verb runAs -ArgumentList $arguments - exit -} - -$ErrorActionPreference = "Stop" - -Write-Host "Updating the live application by rebuilding and restarting the backend service..." -ForegroundColor Yellow - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$root = Resolve-Path (Join-Path $scriptDir "..") -Push-Location $root - -try { - $hostsPath = "C:\Windows\System32\drivers\etc\hosts" - if (Test-Path $hostsPath) { - $hostOverride = Select-String -Path $hostsPath -Pattern "(?im)^\s*127\.0\.0\.1\s+devbits\.ddns\.net(\s|$)" -ErrorAction SilentlyContinue - if ($hostOverride) { - Write-Host "WARNING: hosts file maps devbits.ddns.net to 127.0.0.1. This can cause domain/API connection failures and slow first loads." -ForegroundColor Yellow - Write-Host "Remove that hosts override from $hostsPath (run editor as Administrator) if you want real domain routing." -ForegroundColor Yellow - } - } - - $envFile = Join-Path $root ".env" - if (-not (Test-Path $envFile)) { - throw "Missing $envFile. Create it from backend/.env.example and set strong credentials before deploying." - } - - $envContent = Get-Content -Path $envFile -Raw - if ($envContent -notmatch "(?m)^POSTGRES_PASSWORD=.+$") { - throw "POSTGRES_PASSWORD is not set in $envFile." - } - if ($envContent -match "(?m)^POSTGRES_PASSWORD=(password|changeme|devbits)$") { - throw "POSTGRES_PASSWORD in $envFile is weak/default. Set a strong random value before deploying." - } - - # Ensure DB is started first so we can sync credentials to avoid auth mismatches. - docker compose up -d db - - # Wait for DB health (up to ~60s) - $dbHealthy = $false - for ($attempt = 0; $attempt -lt 30; $attempt++) { - $statusOutput = docker compose ps db 2>$null | Out-String - if ($statusOutput -match "Up" -and $statusOutput -match "healthy") { - $dbHealthy = $true - break - } - Start-Sleep -Seconds 2 - } - - if (-not $dbHealthy) { - Write-Host "Warning: DB did not reach healthy state in time; proceeding to attempt password sync anyway." -ForegroundColor Yellow - } - - # Sync the POSTGRES_PASSWORD from .env into the Postgres role to keep credentials consistent. - try { - $envRaw = Get-Content -Path $envFile -Raw - if ($envRaw -match "(?m)^POSTGRES_PASSWORD=(.+)$") { - $dbPass = $Matches[1].Trim() - if ($dbPass -and $dbPass -notmatch "^(password|changeme|devbits)$") { - $tmpFile = Join-Path $env:TEMP "sync-devbits-password.sql" - $safe = $dbPass.Replace("'", "''") - Set-Content -Path $tmpFile -Value "ALTER ROLE devbits WITH PASSWORD '$safe';" -NoNewline - try { - Get-Content $tmpFile -Raw | docker compose exec -T db sh -lc "psql -U devbits -d postgres" | Out-Null - Write-Host "Synchronized Postgres role password to match .env" -ForegroundColor Green - } - catch { - Write-Host "Warning: Could not run password sync command inside DB container: $_" -ForegroundColor Yellow - } - Remove-Item $tmpFile -Force -ErrorAction SilentlyContinue - } - } - } - catch { - Write-Host "Warning: Failed to read .env or sync password: $_" -ForegroundColor Yellow - } - - docker compose up -d --build backend nginx - Write-Host "Backend and nginx services have been updated." -ForegroundColor Green -} -finally { - Pop-Location -} - -$liveBackendState = "unavailable" -try { - $statusOutput = docker compose -f (Join-Path $root "docker-compose.yml") ps backend 2>$null - if ($LASTEXITCODE -eq 0) { - $liveBackendState = if (($statusOutput | Out-String) -match "Up") { "running" } else { "not running" } - } -} -catch {} - -Write-Host "" -Write-Host "===== Summary =====" -ForegroundColor Cyan -Write-Host "Action: Live backend update executed" -Write-Host "Updated: Backend rebuilt; nginx refreshed" -Write-Host "Live backend: $liveBackendState" - -if (-not $NoPause -and [Environment]::UserInteractive -and $Host.Name -eq "ConsoleHost") { - Read-Host "Press Enter to close" -} diff --git a/backend/scripts/update-live.sh b/backend/scripts/update-live.sh index 8ae65b5..7c24965 100644 --- a/backend/scripts/update-live.sh +++ b/backend/scripts/update-live.sh @@ -1,53 +1,8 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -if [ "$EUID" -ne 0 ]; then - echo "Please run as root" - sudo "$0" "$@" - exit -fi +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -set -e - -echo "Updating the live application by rebuilding and restarting the backend service..." - -# Get the directory of the script -script_dir=$(dirname "$(readlink -f "$0")") -root_dir=$(realpath "$script_dir/..") - -# Change to the root directory of the backend -cd "$root_dir" - -env_file="$root_dir/.env" -if [[ ! -f "$env_file" ]]; then - echo "Missing $env_file. Create it from backend/.env.example and set strong credentials before deploying." >&2 - exit 1 -fi - -postgres_password="$(grep -E '^POSTGRES_PASSWORD=' "$env_file" | tail -n 1 | cut -d '=' -f2- || true)" -if [[ -z "$postgres_password" ]]; then - echo "POSTGRES_PASSWORD is not set in $env_file" >&2 - exit 1 -fi -if [[ "$postgres_password" == "password" || "$postgres_password" == "changeme" || "$postgres_password" == "devbits" ]]; then - echo "POSTGRES_PASSWORD in $env_file is weak/default. Set a strong random value before deploying." >&2 - exit 1 -fi - -docker compose up -d --build backend nginx - -echo "Backend and nginx services have been updated." - -live_backend_state="unavailable" -if backend_status_output="$(docker compose ps backend 2>/dev/null)"; then - if echo "$backend_status_output" | grep -q "Up"; then - live_backend_state="running" - else - live_backend_state="not running" - fi -fi - -echo -echo "===== Summary =====" -echo "Action: Live backend update executed" -echo "Updated: Backend rebuilt; nginx refreshed" -echo "Live backend: $live_backend_state" +echo "Updating live backend (native binary + systemd)..." +"$ROOT_DIR/scripts/deploy-aws-native.sh" diff --git a/backend/uploads/u1_1a5534ccca23297b1a89c65a.jpg b/backend/uploads/u1_1a5534ccca23297b1a89c65a.jpg new file mode 100644 index 0000000..d760285 Binary files /dev/null and b/backend/uploads/u1_1a5534ccca23297b1a89c65a.jpg differ diff --git a/frontend/DEEP_LINK_SETUP.md b/frontend/DEEP_LINK_SETUP.md index d06c6ca..15f56da 100644 --- a/frontend/DEEP_LINK_SETUP.md +++ b/frontend/DEEP_LINK_SETUP.md @@ -55,18 +55,19 @@ Alternative (if you use Play App Signing) 4. Update `assetlinks.json` -- Open `frontend/public/.well-known/assetlinks.json` and replace `` with the SHA-256 fingerprint (format: uppercase hex with colons or without; either is accepted by Android). +- Open `backend/api/static/assetlinks.json` and replace `` with the SHA-256 fingerprint (format: uppercase hex with colons or without; either is accepted by Android). -5. Deploy static files to your server +5. Deploy static files to your server/domain -- Build the frontend static output and copy contents of `frontend/public/` to the nginx static folder (`/usr/share/nginx/html`) so files are reachable at: - - https://devbits.ddns.net/apple-app-site-association - - https://devbits.ddns.net/.well-known/assetlinks.json +- The Go backend now serves these files directly from `backend/api/static/`. No separate static hosting is needed. + - `https://devbits.app/apple-app-site-association` → served from `backend/api/static/apple-app-site-association` (also available at `/.well-known/apple-app-site-association`) + - `https://devbits.app/.well-known/assetlinks.json` → served from `backend/api/static/assetlinks.json` +- To update these files, edit them in `backend/api/static/` and redeploy the backend. 6. Verify - iOS: use Apple's AASA validator or check device logs when opening a Universal Link. -- Android: open `https://devbits.ddns.net/.well-known/assetlinks.json` and use `adb shell am start -a android.intent.action.VIEW -d "https://devbits.ddns.net/some/path"` on a device with the app installed, or check Play Console URL handling tests. +- Android: open `https://devbits.app/.well-known/assetlinks.json` and use `adb shell am start -a android.intent.action.VIEW -d "https://devbits.app/some/path"` on a device with the app installed, or check Play Console URL handling tests. Notes diff --git a/frontend/app.json b/frontend/app.json index a0eb079..132bf87 100644 --- a/frontend/app.json +++ b/frontend/app.json @@ -21,7 +21,7 @@ "NSMicrophoneUsageDescription": "DevBits requests access to the microphone to record audio for posts or streams. Example: recording a short audio clip when creating content." }, "associatedDomains": [ - "applinks:devbits.ddns.net" + "applinks:devbits.app" ] }, "android": { diff --git a/frontend/app/(tabs)/explore.tsx b/frontend/app/(tabs)/explore.tsx index 3bfed92..7dca2be 100644 --- a/frontend/app/(tabs)/explore.tsx +++ b/frontend/app/(tabs)/explore.tsx @@ -104,7 +104,7 @@ export default function ExploreScreen() { const requestGuard = useRequestGuard(); const reveal = useRef(new Animated.Value(0.08)).current; const hasFocusedRef = useRef(false); - const scrollRef = useRef(null); + const scrollRef = useRef(null); const { scrollY, onScroll } = useTopBlurScroll(); useEffect(() => { diff --git a/frontend/app/(tabs)/index.tsx b/frontend/app/(tabs)/index.tsx index 4f4a3d9..f81030f 100644 --- a/frontend/app/(tabs)/index.tsx +++ b/frontend/app/(tabs)/index.tsx @@ -4,7 +4,9 @@ import { InteractionManager, Platform, RefreshControl, + ScrollView, StyleSheet, + ViewStyle, View, } from "react-native"; import { @@ -74,7 +76,7 @@ export default function HomeScreen() { const motion = useMotionConfig(); const hyprMotion = useHyprMotion(); const requestGuard = useRequestGuard(); - const scrollRef = useRef(null); + const scrollRef = useRef(null); const hasFocusedRef = useRef(false); const { scrollY, onScroll } = useTopBlurScroll(); const heroProgress = useSharedValue(0.08); @@ -350,28 +352,28 @@ export default function HomeScreen() { useAutoRefresh(() => loadFeed(false), { focusRefresh: false }); - const heroRevealStyle = useAnimatedStyle(() => ({ + const heroRevealStyle = useAnimatedStyle(() => ({ opacity: heroProgress.value, transform: [ { translateY: (1 - heroProgress.value) * 22 }, { scale: 0.97 + heroProgress.value * 0.03 }, - ], + ] as any, })); - const streamsRevealStyle = useAnimatedStyle(() => ({ + const streamsRevealStyle = useAnimatedStyle(() => ({ opacity: streamsProgress.value, transform: [ { translateY: (1 - streamsProgress.value) * 22 }, { scale: 0.97 + streamsProgress.value * 0.03 }, - ], + ] as any, })); - const postsRevealStyle = useAnimatedStyle(() => ({ + const postsRevealStyle = useAnimatedStyle(() => ({ opacity: postsProgress.value, transform: [ { translateY: (1 - postsProgress.value) * 22 }, { scale: 0.97 + postsProgress.value * 0.03 }, - ], + ] as any, })); const cursorStyle = useAnimatedStyle(() => ({ diff --git a/frontend/app/bytes.tsx b/frontend/app/bytes.tsx index 430ecfc..1ad977f 100644 --- a/frontend/app/bytes.tsx +++ b/frontend/app/bytes.tsx @@ -487,6 +487,12 @@ const styles = StyleSheet.create({ paddingTop: 8, flexWrap: "wrap", }, + filterRow: { + flexDirection: "row", + gap: 8, + paddingTop: 8, + flexWrap: "wrap", + }, filterChip: { paddingVertical: 6, paddingHorizontal: 10, diff --git a/frontend/app/post/[postId].tsx b/frontend/app/post/[postId].tsx index eaf99d5..0562245 100644 --- a/frontend/app/post/[postId].tsx +++ b/frontend/app/post/[postId].tsx @@ -14,6 +14,7 @@ import { Platform, Pressable, RefreshControl, + ScrollView, StyleSheet, TextInput, TouchableWithoutFeedback, @@ -97,7 +98,7 @@ export default function PostDetailScreen() { const { isSaved, savedPostIds, toggleSave } = useSaved(); const motion = useMotionConfig(); const reveal = useRef(new Animated.Value(0.08)).current; - const scrollRef = useRef(null); + const scrollRef = useRef(null); const { scrollY, onScroll } = useTopBlurScroll(); const [post, setPost] = useState(null); const [project, setProject] = useState(null); diff --git a/frontend/app/saved-streams.tsx b/frontend/app/saved-streams.tsx index 21223cf..bc1e84d 100644 --- a/frontend/app/saved-streams.tsx +++ b/frontend/app/saved-streams.tsx @@ -1,5 +1,11 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; -import { Animated, RefreshControl, StyleSheet, View } from "react-native"; +import { + Animated, + RefreshControl, + ScrollView, + StyleSheet, + View, +} from "react-native"; import { SafeAreaView, useSafeAreaInsets, @@ -35,7 +41,7 @@ export default function SavedStreamsScreen() { ); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); - const scrollRef = useRef(null); + const scrollRef = useRef(null); const motion = useMotionConfig(); const reveal = useRef(new Animated.Value(0.08)).current; const { scrollY, onScroll } = useTopBlurScroll(); diff --git a/frontend/app/settings/about.tsx b/frontend/app/settings/about.tsx index 0fdcb4f..8b4be32 100644 --- a/frontend/app/settings/about.tsx +++ b/frontend/app/settings/about.tsx @@ -8,7 +8,7 @@ import { useAppColors } from "@/hooks/useAppColors"; import { SettingsPageShell, settingsStyles } from "@/features/settings/shared"; const SITE_BASE_URL = ( - process.env.EXPO_PUBLIC_SITE_URL?.trim() || "https://devbits.ddns.net" + process.env.EXPO_PUBLIC_SITE_URL?.trim() || "https://devbits.app" ).replace(/\/+$/, ""); const publicLinks = [ diff --git a/frontend/app/settings/index.tsx b/frontend/app/settings/index.tsx index 05341ba..eb1b22f 100644 --- a/frontend/app/settings/index.tsx +++ b/frontend/app/settings/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from "react"; -import { Animated, StyleSheet, View } from "react-native"; +import { Animated, ScrollView, StyleSheet, View } from "react-native"; import { SafeAreaView, useSafeAreaInsets, @@ -20,7 +20,7 @@ export default function SettingsHubScreen() { const router = useRouter(); const motion = useMotionConfig(); const reveal = useRef(new Animated.Value(0.08)).current; - const scrollRef = useRef(null); + const scrollRef = useRef(null); const { scrollY, onScroll } = useTopBlurScroll(); const { user } = useAuth(); diff --git a/frontend/app/stream/[projectId].tsx b/frontend/app/stream/[projectId].tsx index feac932..5365d8d 100644 --- a/frontend/app/stream/[projectId].tsx +++ b/frontend/app/stream/[projectId].tsx @@ -9,6 +9,7 @@ import { Animated, Pressable, RefreshControl, + ScrollView, StyleSheet, View, } from "react-native"; @@ -108,7 +109,7 @@ export default function StreamDetailScreen() { const bottom = useBottomTabOverflow(); const motion = useMotionConfig(); const reveal = useRef(new Animated.Value(0.08)).current; - const scrollRef = useRef(null); + const scrollRef = useRef(null); const { scrollY, onScroll } = useTopBlurScroll(); const hasLoadedOnceRef = useRef(false); const loadSequenceRef = useRef(0); diff --git a/frontend/app/user/[username].tsx b/frontend/app/user/[username].tsx index 7d3fe7a..768b875 100644 --- a/frontend/app/user/[username].tsx +++ b/frontend/app/user/[username].tsx @@ -11,6 +11,7 @@ import { Modal, Pressable, RefreshControl, + ScrollView, StyleSheet, TextInput, View, @@ -87,7 +88,7 @@ export default function UserProfileScreen() { const motion = useMotionConfig(); const reveal = useRef(new Animated.Value(0.08)).current; const hasLoadedRef = useRef(false); - const scrollRef = useRef(null); + const scrollRef = useRef(null); const { scrollY, onScroll } = useTopBlurScroll(); const filteredFollowerUsers = useMemo(() => { diff --git a/frontend/components/FadeInImage.tsx b/frontend/components/FadeInImage.tsx index 818b1d5..8d2a94c 100644 --- a/frontend/components/FadeInImage.tsx +++ b/frontend/components/FadeInImage.tsx @@ -148,9 +148,9 @@ export function FadeInImage({ onError?.(event); }; - const handleLoadEnd: ImageProps["onLoadEnd"] = (event) => { + const handleLoadEnd: ImageProps["onLoadEnd"] = () => { reveal(); - onLoadEnd?.(event); + onLoadEnd?.(); }; if (loadFailed) { diff --git a/frontend/components/HyprBackdrop.tsx b/frontend/components/HyprBackdrop.tsx index c4f3303..6317020 100644 --- a/frontend/components/HyprBackdrop.tsx +++ b/frontend/components/HyprBackdrop.tsx @@ -34,11 +34,13 @@ export function HyprBackdrop() { }); const baseColors = - scheme === "dark" ? ["#0a1110", "#0e1715"] : ["#f2f7f3", "#e6f2eb"]; + scheme === "dark" + ? (["#0a1110", "#0e1715"] as const) + : (["#f2f7f3", "#e6f2eb"] as const); const glowColors = scheme === "dark" - ? ["rgba(58, 227, 186, 0.18)", "rgba(51, 184, 255, 0.12)"] - : ["rgba(61, 211, 176, 0.22)", "rgba(77, 176, 255, 0.18)"]; + ? (["rgba(58, 227, 186, 0.18)", "rgba(51, 184, 255, 0.12)"] as const) + : (["rgba(61, 211, 176, 0.22)", "rgba(77, 176, 255, 0.18)"] as const); return ( diff --git a/frontend/components/TopBlur.tsx b/frontend/components/TopBlur.tsx index ecfffbd..a58dc08 100644 --- a/frontend/components/TopBlur.tsx +++ b/frontend/components/TopBlur.tsx @@ -15,15 +15,19 @@ export function TopBlur({ scrollY }: TopBlurProps) { const height = Math.max(insets.top, 12) + 28; const veilColors = useMemo(() => { if (theme === "dark") { - return ["rgba(0, 0, 0, 0.24)", "rgba(0, 0, 0, 0.12)", "rgba(0, 0, 0, 0)"]; + return [ + "rgba(0, 0, 0, 0.24)", + "rgba(0, 0, 0, 0.12)", + "rgba(0, 0, 0, 0)", + ] as const; } return [ "rgba(255, 255, 255, 0.2)", "rgba(255, 255, 255, 0.1)", "rgba(255, 255, 255, 0)", - ]; + ] as const; }, [theme]); - const veilStops = [0, 0.5, 1]; + const veilStops = [0, 0.5, 1] as const; const opacity = scrollY ? scrollY.interpolate({ inputRange: [0, 60], diff --git a/frontend/components/__tests__/ThemedText-test.tsx b/frontend/components/__tests__/ThemedText-test.tsx index 2a11b4d..8870e98 100644 --- a/frontend/components/__tests__/ThemedText-test.tsx +++ b/frontend/components/__tests__/ThemedText-test.tsx @@ -4,9 +4,33 @@ import renderer from "react-test-renderer"; import { ThemedText } from "../ThemedText"; +jest.mock("@/contexts/PreferencesContext", () => ({ + usePreferences: () => ({ + preferences: { + zenMode: false, + compactMode: false, + textRenderEffect: "off", + }, + }), +})); + +jest.mock("@/hooks/useThemeColor", () => ({ + useThemeColor: () => "#000", +})); + it("renders correctly", () => { - const testRenderer = renderer.create(Snapshot test!); + let testRenderer: renderer.ReactTestRenderer; + renderer.act(() => { + testRenderer = renderer.create( + + Snapshot test! + , + ); + }); - const textNode = testRenderer.root.findByType(Text); + const textNode = testRenderer!.root.findByType(Text); expect(textNode.props.children).toBe("Snapshot test!"); + renderer.act(() => { + testRenderer!.unmount(); + }); }); diff --git a/frontend/eas.json b/frontend/eas.json index aee494a..763fcd5 100644 --- a/frontend/eas.json +++ b/frontend/eas.json @@ -14,9 +14,9 @@ "production": { "autoIncrement": true, "env": { - "EXPO_PUBLIC_API_URL": "https://devbits.ddns.net", - "EXPO_PUBLIC_API_FALLBACK_URL": "https://devbits.ddns.net", - "EXPO_PUBLIC_SITE_URL": "https://devbits.ddns.net" + "EXPO_PUBLIC_API_URL": "https://devbits.app", + "EXPO_PUBLIC_API_FALLBACK_URL": "https://devbits.app", + "EXPO_PUBLIC_SITE_URL": "https://devbits.app" } , "credentialsSource": "remote" diff --git a/frontend/public/account-deletion.html b/frontend/public/account-deletion.html index 12e5725..8828e83 100644 --- a/frontend/public/account-deletion.html +++ b/frontend/public/account-deletion.html @@ -237,7 +237,7 @@

How to request deletion

  1. Email us at - mail@elifouts.com with the + mail@elifouts.net with the subject "Account Deletion Request" and include the username and the email address associated with your account.
  2. diff --git a/frontend/public/privacy-policy.html b/frontend/public/privacy-policy.html index 845d695..0227e39 100644 --- a/frontend/public/privacy-policy.html +++ b/frontend/public/privacy-policy.html @@ -222,7 +222,7 @@

    Introduction & Organizational Information

    privacy and to handling personal information in a responsible manner. This policy describes how we collect, use, disclose, and protect personal information through our website - devbits.ddns.net, our mobile + devbits.app, our mobile applications (Devbits on Android and iOS), and related services.

    diff --git a/frontend/services/api.ts b/frontend/services/api.ts index b31fe29..899fdc0 100644 --- a/frontend/services/api.ts +++ b/frontend/services/api.ts @@ -214,7 +214,7 @@ const getHostFromUri = (uri?: string | null) => { const getDefaultBaseUrl = () => { // Check for overrides provided via Expo `extra` or environment variables. // This allows running the Expo client locally while targeting a remote - // backend (for example devbits.ddns.net) from any developer machine. + // backend (for example devbits.app) from any developer machine. try { const extras = (Constants as any)?.expoConfig?.extra ?? (Constants as any)?.manifest2?.extra ?? @@ -257,9 +257,9 @@ const getDefaultBaseUrl = () => { } // If the developer explicitly disables using the local API in dev, use - // the production DDNS endpoint even when __DEV__ is true. + // the production endpoint even when __DEV__ is true. if (!useLocal) { - return "https://devbits.ddns.net"; + return "https://devbits.app"; } } catch { // ignore and fall back to defaults below @@ -294,7 +294,7 @@ const getDefaultBaseUrl = () => { } // Production: use live server - return "https://devbits.ddns.net"; + return "https://devbits.app"; }; const normalizeBaseUrl = (url: string) => url.replace(/\/+$/, ""); @@ -318,30 +318,30 @@ const buildBaseUrlList = (...candidates: Array) => { return urls; }; -export const API_BASE_URL = normalizeBaseUrl(getDefaultBaseUrl()); - -// Defensive runtime validation: if the resolved URL is malformed (e.g. "http://" -// with no host) fall back to the public DDNS host and log an error so developers -// can see the problem in the Metro/Expo console. -try { - const checkUrl = new URL(API_BASE_URL); - if (!checkUrl.hostname) { - // eslint-disable-next-line no-console - console.error("Invalid API_BASE_URL resolved; falling back to https://devbits.ddns.net", API_BASE_URL); - // normalize and overwrite - (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.ddns.net"); - } -} catch (e) { - // If parsing fails entirely, fallback and log. +// Validate the resolved URL at module load time; if it is malformed (e.g. +// "http://" with no host) fall back to the public host and log an error so +// developers can see the problem in the Metro/Expo console. +function getValidatedBaseUrl(): string { + const raw = normalizeBaseUrl(getDefaultBaseUrl()); try { + const checkUrl = new URL(raw); + if (!checkUrl.hostname) { + // eslint-disable-next-line no-console + console.error("Invalid API_BASE_URL resolved; falling back to https://devbits.app", raw); + return normalizeBaseUrl("https://devbits.app"); + } + return raw; + } catch (e) { // eslint-disable-next-line no-console - console.error("Failed to parse API_BASE_URL; falling back to https://devbits.ddns.net", API_BASE_URL, String(e)); - } catch {} - (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.ddns.net"); + console.error("Failed to parse API_BASE_URL; falling back to https://devbits.app", raw, String(e)); + return normalizeBaseUrl("https://devbits.app"); + } } +export const API_BASE_URL = getValidatedBaseUrl(); + const API_FALLBACK_URL = normalizeBaseUrl( - __DEV__ ? "" : "https://devbits.ddns.net", + __DEV__ ? "" : "https://devbits.app", ); const API_REQUEST_BASE_URLS = buildBaseUrlList(API_BASE_URL, API_FALLBACK_URL); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 8466a87..7164bec 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,14 +1,12 @@ { + "extends": "expo/tsconfig.base", "compilerOptions": { - "ignoreDeprecations": "6.0", "baseUrl": ".", "paths": { "@/*": ["./*"] }, - "moduleResolution": "bundler", - "types": ["jest", "react", "react-native"] + "types": ["jest"] }, - "extends": "expo/tsconfig.base", "include": [ "**/*.ts", "**/*.tsx",