Files
ForbiddenStarsApp/run.ps1
NotBigGhostandClaude Opus 5 cff48f7cc7 Безопасность: опубликованный dev не стартует с дефолтными секретами
При LOCAL_PUBLIC=vps dev доступен на forbidden-stars.ru, а fail-fast по
SECRET_KEY/ADMIN_PASSWORD работал только в production: снаружи оставались
общеизвестный ключ JWT (подделка любого токена, включая админский) и пароль
админки. Теперь проверка срабатывает при is_published — у прода и у dev на
домене; на нём же cookie_secure.

Dev-инструменты и Swagger на опубликованном dev остаются (решение владельца):
лаунчеры и лог старта перечисляют, что открыто любому посетителю. В .env.example —
что открывает vps и что у dev и prod должны быть разные SECRET_KEY. #69

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
2026-09-18 23:52:25 +03:00

146 lines
7.2 KiB
PowerShell

#!/usr/bin/env pwsh
# Unified launcher for dev. APP_ENV in the root .env decides what to run.
# development -> uvicorn --reload (backend) + vite (frontend), native, two windows
# production -> NOT started by launcher (prod is separate: docker compose up -d on Pi)
#
# LOCAL_PUBLIC=vps opens an SSH tunnel to the VPS, exposing dev at
# https://forbidden-stars.ru. Prod exposes itself via an in-container tunnel.
#
# Run: .\run.ps1
# If blocked by policy: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
#
# NOTE: keep this file ASCII-only. Windows PowerShell 5.1 reads BOM-less .ps1 in the
# system ANSI codepage, so non-ASCII (Cyrillic / box chars) breaks the parser.
$ErrorActionPreference = "Stop"
$root = $PSScriptRoot
$envFile = Join-Path $root ".env"
if (-not (Test-Path $envFile)) {
Write-Host "No .env in repo root. Create it: Copy-Item .env.example .env" -ForegroundColor Red
exit 1
}
# Read a key from .env (last assignment wins; comments ignored).
function Get-EnvVal([string]$name, [string]$default) {
$val = $default
foreach ($line in Get-Content $envFile) {
if ($line -match "^\s*$name\s*=\s*([^#\s]+)") { $val = $matches[1].Trim() }
}
return $val
}
$appEnv = (Get-EnvVal "APP_ENV" "development").ToLower()
$localPublic = (Get-EnvVal "LOCAL_PUBLIC" "local").ToLower()
$vpsHost = Get-EnvVal "VPS_TUNNEL_HOST" ""
$vpsUser = Get-EnvVal "VPS_TUNNEL_USER" "tunnel"
$vpsPort = Get-EnvVal "VPS_TUNNEL_PORT" "9001"
Write-Host "APP_ENV = $appEnv LOCAL_PUBLIC = $localPublic" -ForegroundColor Cyan
# Open SSH reverse tunnel VPS:$vpsPort -> localhost:$localPort (in a separate window).
function Start-VpsTunnel([int]$localPort) {
if (-not $vpsHost) {
Write-Host "LOCAL_PUBLIC=vps but VPS_TUNNEL_HOST is empty in .env - tunnel skipped." -ForegroundColor Red
return
}
Write-Host "SSH tunnel: forbidden-stars.ru (VPS:$vpsPort) -> localhost:$localPort ..." -ForegroundColor Green
$ssh = "ssh -N -R ${vpsPort}:localhost:${localPort} " +
"-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes " +
"${vpsUser}@${vpsHost}"
Start-Process powershell -ArgumentList @(
"-NoExit", "-Command",
"Write-Host 'SSH tunnel to forbidden-stars.ru. Close this window to stop.' -ForegroundColor Cyan; $ssh"
)
}
# Kill any leftover tunnel from a previous run, so LOCAL_PUBLIC is authoritative each launch
# (the tunnel lives in its own window and would otherwise keep the domain served).
function Stop-VpsTunnel {
Get-CimInstance Win32_Process -Filter "Name='ssh.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.CommandLine -match "-R\s+${vpsPort}:localhost" } |
ForEach-Object {
Write-Host "Closing leftover tunnel (pid $($_.ProcessId))..." -ForegroundColor DarkYellow
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
}
}
# Start from a clean tunnel state; a fresh tunnel is opened only if LOCAL_PUBLIC=vps below.
Stop-VpsTunnel
switch ($appEnv) {
"development" {
$backend = Join-Path $root "backend"
$frontend = Join-Path $root "frontend"
$py = Join-Path $backend ".venv\Scripts\python.exe"
if (-not (Test-Path $py)) {
Write-Host "venv not found: $py" -ForegroundColor Red
Write-Host "First: cd backend; python -m venv .venv; .\.venv\Scripts\Activate.ps1; pip install -e `".[dev]`"" -ForegroundColor Yellow
exit 1
}
if (-not (Test-Path (Join-Path $frontend "node_modules"))) {
Write-Host "node_modules not found. First: cd frontend; npm install" -ForegroundColor Red
exit 1
}
# Schema first: a branch may add a migration, and uvicorn would otherwise start
# on the old schema and fail with 500 on the first hit of a new table.
# Start-Process, not "& python ... 2> file": alembic logs to stderr, and in
# Windows PowerShell 5.1 ANY stderr redirection of a native exe turns each line
# into a NativeCommandError - which is terminating under $ErrorActionPreference
# = "Stop" set at the top of this script, so the launcher died right here.
Write-Host "Applying migrations (alembic upgrade head)..." -ForegroundColor Green
$migOut = [System.IO.Path]::GetTempFileName()
$migErr = [System.IO.Path]::GetTempFileName()
$mig = Start-Process -FilePath $py `
-ArgumentList "-m", "alembic", "upgrade", "head" `
-WorkingDirectory $backend -NoNewWindow -Wait -PassThru `
-RedirectStandardOutput $migOut -RedirectStandardError $migErr
$migCode = $mig.ExitCode
$migText = ((Get-Content $migErr -Raw), (Get-Content $migOut -Raw)) -join "`n"
Remove-Item $migOut -Force -ErrorAction SilentlyContinue
Remove-Item $migErr -Force -ErrorAction SilentlyContinue
if ($migCode -ne 0) {
Write-Host $migText
if ($migText -match "locate revision") {
# The DB carries a migration this branch does not have (switched back from
# a feature branch). Extra tables do not bother the older code - go on.
Write-Host "DB is newer than this branch - migrations skipped." -ForegroundColor Yellow
} else {
Write-Host "Migrations failed - not starting. Fix the error above and retry." -ForegroundColor Red
exit 1
}
}
Write-Host "Starting backend (uvicorn --reload) and frontend (vite) in separate windows..." -ForegroundColor Green
# --timeout-graceful-shutdown: an open tab keeps the SSE stream /api/events forever,
# and on reload the old process waits for ALL connections to close - with no limit
# the reload never finishes and the site hangs on loading with nothing in the log.
Start-Process powershell -ArgumentList @(
"-NoExit", "-Command",
"Set-Location '$backend'; & '$py' -m uvicorn app.main:app --reload --timeout-graceful-shutdown 2"
)
Start-Process powershell -ArgumentList @(
"-NoExit", "-Command",
"Set-Location '$frontend'; npm run dev"
)
Write-Host ""
Write-Host " Backend: http://127.0.0.1:8000 (Swagger: /api/docs)" -ForegroundColor Green
Write-Host " Frontend: http://127.0.0.1:5173" -ForegroundColor Green
if ($localPublic -eq "vps") {
Start-VpsTunnel 5173
Write-Host " Public: https://forbidden-stars.ru" -ForegroundColor Green
Write-Host " WARNING: dev is public. Anyone can log in by nickname without a password," -ForegroundColor Yellow
Write-Host " list/create players, hard-delete accounts and read Swagger." -ForegroundColor Yellow
Write-Host " Do not keep a copy of production data in the dev database." -ForegroundColor Yellow
}
}
"production" {
Write-Host "production is not started by the launcher - prod is separate." -ForegroundColor Yellow
Write-Host "Deploy on Pi (from main branch): docker compose up -d --build"
exit 1
}
default {
Write-Host "Unknown APP_ENV='$appEnv'. Allowed: development | production." -ForegroundColor Red
exit 1
}
}