From 2f76b283390fe83711f19e746516e46393b3d9b7 Mon Sep 17 00:00:00 2001 From: lendry Date: Thu, 2 Jul 2026 16:19:06 +0300 Subject: [PATCH] fix and update --- .env.example | 2 + .gitignore | 1 + apps/frontend/app/download/android/route.ts | 2 +- install.sh | 67 ++++++++++++++++- tauri_app/README.md | 23 +++++- tauri_app/docker/build-android-apk.sh | 15 +++- tauri_app/docker/ensure-android-keystore.sh | 79 +++++++++++++++++++++ tauri_app/docker/patch-android-signing.sh | 61 ++++++++++++++++ tauri_app/docker/verify-apk-signature.sh | 21 ++++++ 9 files changed, 262 insertions(+), 9 deletions(-) create mode 100644 tauri_app/docker/ensure-android-keystore.sh create mode 100644 tauri_app/docker/patch-android-signing.sh create mode 100644 tauri_app/docker/verify-apk-signature.sh diff --git a/.env.example b/.env.example index 6951e96..50826c4 100644 --- a/.env.example +++ b/.env.example @@ -74,4 +74,6 @@ DATA_ENCRYPTION_KEY=change-me-data-encryption-key-32-chars-min # NPM registry (опционально) NPM_REGISTRY=https://registry.npmjs.org +# Android APK (tauri_app): keystore создаётся автоматически в tauri_app/.secrets/ при ./install.sh --build-apk + COMPOSE_PROJECT_NAME=lendry-id diff --git a/.gitignore b/.gitignore index 07cb4d7..743cc7d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ apps/media-ws/media-ws.exe tauri_app/docker/agp-resolver/.gradle/ tauri_app/docker/agp-resolver/build/ tauri_app/docker/offline-gradle/*.zip +tauri_app/.secrets/ diff --git a/apps/frontend/app/download/android/route.ts b/apps/frontend/app/download/android/route.ts index 4714e1b..6f244bb 100644 --- a/apps/frontend/app/download/android/route.ts +++ b/apps/frontend/app/download/android/route.ts @@ -24,7 +24,7 @@ export async function GET() { const apkPath = resolveApkPath(); if (!apkPath) { return new Response( - 'APK ещё не собран. Запустите: docker compose up --build tauri-apk-builder. После сборки файл появится как apps/frontend/public/downloads/lendry-id.apk.', + 'APK ещё не собран. Запустите: ./install.sh --build-apk (или пункт 12 в меню). После сборки файл появится как apps/frontend/public/downloads/lendry-id.apk.', { status: 404, headers: { diff --git a/install.sh b/install.sh index d94b4a0..c9e688d 100644 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ set -euo pipefail -SCRIPT_VERSION="2.6.4" +SCRIPT_VERSION="2.6.5" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" @@ -83,6 +83,7 @@ ${BOLD}Lendry ID v${SCRIPT_VERSION}${NC} — установка и управл ./install.sh --fix-all Комплексное исправление: 502, прокси, порты, Nginx ./install.sh --status Статус сервисов ./install.sh --reset-db Сброс PostgreSQL (новый пароль из .env, данные удалятся) + ./install.sh --build-apk Собрать подписанный Android APK (tauri_app → frontend/public/downloads) ./install.sh --remove-nginx Удалить конфиги Nginx IdP ${BOLD}Режим локальной сети (без интернета / AD-домены .local .lpr):${NC} @@ -170,6 +171,8 @@ parse_args() { --fix-all) ACTION="fix-all"; shift ;; --status) ACTION="status"; shift ;; --reset-db) ACTION="reset-db"; shift ;; + --build-apk) ACTION="build-apk"; shift ;; + --remove-nginx) ACTION="remove-nginx"; shift ;; --local) INSTALL_LOCAL=1; shift ;; --intranet) INSTALL_INTRANET=1; shift ;; --offline) OFFLINE=1; shift ;; @@ -4470,6 +4473,63 @@ action_fix_all_errors() { show_status } +action_build_tauri_apk() { + local image="${TAURI_APK_IMAGE:-lendry-id-tauri-apk-builder}" + local output_dir="${ROOT_DIR}/apps/frontend/public/downloads" + local output_apk="${output_dir}/lendry-id.apk" + local secrets_dir="${ROOT_DIR}/tauri_app/.secrets" + local public_api public_frontend npm_registry + + load_env + public_api="$(env_get PUBLIC_API_URL "http://localhost:3000")" + public_frontend="$(env_get PUBLIC_FRONTEND_URL "http://localhost:3002")" + npm_registry="$(env_get NPM_REGISTRY "https://registry.npmjs.org")" + + mkdir -p "${output_dir}" "${secrets_dir}" + chmod 700 "${secrets_dir}" 2>/dev/null || true + + echo "" + echo -e "${BOLD}Сборка подписанного Android APK (Tauri)${NC}" + echo " API: ${public_api}" + echo " Frontend: ${public_frontend}" + echo " Выход: ${output_apk}" + echo "" + + if [[ "${OFFLINE}" -eq 1 ]]; then + warn "OFFLINE=1: образ APK-builder должен быть уже собран локально" + fi + + log "Шаг 1/3: Docker-образ tauri_app (Android SDK + NDK)..." + configure_docker_build_env + docker_cmd build \ + -f "${ROOT_DIR}/tauri_app/Dockerfile.apk" \ + -t "${image}" \ + --build-arg "NPM_REGISTRY=${npm_registry}" \ + "${ROOT_DIR}" + + log "Шаг 2/3: Сборка release APK с подписью..." + docker_cmd run --rm \ + -v "${output_dir}:/out" \ + -v "${secrets_dir}:/workspace/tauri_app/.secrets" \ + -e "PUBLIC_API_URL=${public_api}" \ + -e "PUBLIC_FRONTEND_URL=${public_frontend}" \ + -e "VITE_API_URL=${public_api}" \ + -e "VITE_FRONTEND_URL=${public_frontend}" \ + "${image}" + + log "Шаг 3/3: Проверка результата..." + [[ -f "${output_apk}" ]] || fail "APK не найден после сборки: ${output_apk}" + + ok "Подписанный APK готов: ${output_apk}" + ls -lh "${output_apk}" 2>/dev/null || true + if [[ -f "${secrets_dir}/signing-credentials.env" ]]; then + warn "Пароли keystore: ${secrets_dir}/signing-credentials.env (не коммитьте в git)" + fi + echo "" + echo " Скачивание на сайте: /download/android" + echo " Перезапустите frontend, если контейнер уже работает: ./install.sh --restart" +} + action_restart() { load_env prepare_compose_stack @@ -4496,6 +4556,7 @@ show_menu() { echo " 3) SSL-сертификаты (обновить / включить custom / Let's Encrypt)" echo " 4) Перезапуск / пересборка контейнеров (+ очистка Docker)" echo " 11) Миграция БД и пересборка (после git pull)" + echo " 12) Собрать Android APK (Tauri, подписанный → frontend/public/downloads)" echo " 5) Статус сервисов" echo " 6) Удалить конфиги Nginx IdP" echo " 7) Сброс PostgreSQL (если ошибка P1000 / пароль БД)" @@ -4505,7 +4566,7 @@ show_menu() { echo "" local choice - read -r -p "Выберите пункт [0-11]: " choice + read -r -p "Выберите пункт [0-12]: " choice case "$choice" in 1) NON_INTERACTIVE=0; action_full_install ;; @@ -4514,6 +4575,7 @@ show_menu() { 3) action_renew_ssl ;; 4) action_restart ;; 11) action_migrate_rebuild ;; + 12) action_build_tauri_apk ;; 5) show_status ;; 6) remove_nginx_configs; ok "Готово" ;; 7) action_reset_db; action_restart ;; @@ -4540,6 +4602,7 @@ main() { renew-ssl) action_renew_ssl ;; restart) action_restart ;; migrate-rebuild) action_migrate_rebuild ;; + build-apk) action_build_tauri_apk ;; fix-host-nginx|fix-docker-nginx) action_fix_docker_nginx ;; docker-prune) action_docker_prune ;; fix-proxy) action_fix_host_proxy ;; diff --git a/tauri_app/README.md b/tauri_app/README.md index f0d77f7..2bfda2f 100644 --- a/tauri_app/README.md +++ b/tauri_app/README.md @@ -30,22 +30,39 @@ npm --workspace @lendry/tauri-app run ios:init ## Docker APK build -При запуске общего `docker compose up --build` сервис `tauri-apk-builder` собирает Android APK и кладёт файл сюда: +Сборка подписанного release APK через `install.sh` (Docker + Android SDK + release keystore): + +```bash +./install.sh --build-apk +``` + +Или пункт **12** в интерактивном меню `./install.sh`. + +Готовый файл: ```text apps/frontend/public/downloads/lendry-id.apk ``` +Keystore и пароли создаются автоматически при первой сборке и сохраняются в `tauri_app/.secrets/` (не коммитятся в git). + После этого кнопка «Скачать приложение» на странице входа скачивает APK через: ```text /download/android ``` -Собрать только APK: +Собрать только через Docker-образ (без install.sh): ```bash -docker compose up --build tauri-apk-builder +docker build -f tauri_app/Dockerfile.apk -t lendry-id-tauri-apk-builder . +mkdir -p apps/frontend/public/downloads tauri_app/.secrets +docker run --rm \ + -v "$(pwd)/apps/frontend/public/downloads:/out" \ + -v "$(pwd)/tauri_app/.secrets:/workspace/tauri_app/.secrets" \ + -e PUBLIC_API_URL=https://sso.example.ru/idp-api \ + -e PUBLIC_FRONTEND_URL=https://sso.example.ru \ + lendry-id-tauri-apk-builder ``` ## Переменные окружения diff --git a/tauri_app/docker/build-android-apk.sh b/tauri_app/docker/build-android-apk.sh index 653ded7..4265c7a 100644 --- a/tauri_app/docker/build-android-apk.sh +++ b/tauri_app/docker/build-android-apk.sh @@ -50,17 +50,26 @@ echo "==> Настройка Android SDK (offline, без загрузки platf echo "==> Настройка offline Gradle wrapper (если bundled zip доступен)" /workspace/tauri_app/docker/install-offline-gradle.sh /workspace/tauri_app/src-tauri/gen/android -echo "==> Сборка Android APK" +echo "==> Подготовка keystore для release-подписи APK" +/workspace/tauri_app/docker/ensure-android-keystore.sh +/workspace/tauri_app/docker/patch-android-signing.sh /workspace/tauri_app/src-tauri/gen/android + +echo "==> Сборка Android APK (release, signed)" npm run tauri -- android build --apk -APK_PATH="$(find /workspace/tauri_app/src-tauri/gen/android -type f -name '*.apk' | sort | tail -n 1 || true)" +APK_PATH="$(find /workspace/tauri_app/src-tauri/gen/android -type f -name '*release*.apk' ! -name '*unsigned*' | sort | tail -n 1 || true)" +if [[ -z "${APK_PATH}" ]]; then + APK_PATH="$(find /workspace/tauri_app/src-tauri/gen/android -type f -name '*.apk' ! -name '*unsigned*' | sort | tail -n 1 || true)" +fi if [ -z "${APK_PATH}" ] || [ ! -f "${APK_PATH}" ]; then echo "APK не найден после сборки" >&2 - find /workspace/tauri_app/src-tauri/gen/android -maxdepth 8 -type f | sort >&2 || true + find /workspace/tauri_app/src-tauri/gen/android -maxdepth 8 -type f -name '*.apk' | sort >&2 || true exit 1 fi +/workspace/tauri_app/docker/verify-apk-signature.sh "${APK_PATH}" + cp "${APK_PATH}" /out/lendry-id.apk chmod 0644 /out/lendry-id.apk diff --git a/tauri_app/docker/ensure-android-keystore.sh b/tauri_app/docker/ensure-android-keystore.sh new file mode 100644 index 0000000..10fe60e --- /dev/null +++ b/tauri_app/docker/ensure-android-keystore.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Создаёт release keystore для подписи APK, если его ещё нет. +set -euo pipefail + +SECRETS_DIR="${ANDROID_SECRETS_DIR:-/workspace/tauri_app/.secrets}" +KEYSTORE_FILE="${ANDROID_KEYSTORE_FILE:-${SECRETS_DIR}/lendry-id-release.keystore}" +KEY_ALIAS="${ANDROID_KEY_ALIAS:-lendry-id}" +CREDENTIALS_FILE="${SECRETS_DIR}/signing-credentials.env" +KEYSTORE_PROPS_TEMPLATE="${SECRETS_DIR}/keystore.properties.template" + +mkdir -p "${SECRETS_DIR}" +chmod 700 "${SECRETS_DIR}" || true + +if [[ -f "${KEYSTORE_FILE}" ]]; then + echo "==> Android keystore уже существует: ${KEYSTORE_FILE}" + if [[ ! -f "${PROPS_TEMPLATE}" && -f "${CREDENTIALS_FILE}" ]]; then + # shellcheck disable=SC1090 + source "${CREDENTIALS_FILE}" + cat > "${PROPS_TEMPLATE}" < Восстановлен keystore.properties.template из signing-credentials.env" + fi + exit 0 +fi + +if ! command -v keytool >/dev/null 2>&1; then + echo "keytool не найден — нужен JDK для генерации keystore" >&2 + exit 1 +fi + +gen_secret() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 24 + else + date +%s%N | sha256sum | head -c 24 + fi +} + +STORE_PASSWORD="$(gen_secret)" +KEY_PASSWORD="$(gen_secret)" + +echo "==> Генерация release keystore для подписи APK..." +keytool -genkeypair -v \ + -storetype PKCS12 \ + -keystore "${KEYSTORE_FILE}" \ + -alias "${KEY_ALIAS}" \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -storepass "${STORE_PASSWORD}" \ + -keypass "${KEY_PASSWORD}" \ + -dname "CN=Lendry ID, OU=Mobile, O=Lendry, C=RU" + +chmod 600 "${KEYSTORE_FILE}" || true + +cat > "${CREDENTIALS_FILE}" < "${KEYSTORE_PROPS_TEMPLATE}" < Keystore создан: ${KEYSTORE_FILE}" +echo "==> Пароли сохранены: ${CREDENTIALS_FILE}" diff --git a/tauri_app/docker/patch-android-signing.sh b/tauri_app/docker/patch-android-signing.sh new file mode 100644 index 0000000..a9ac33f --- /dev/null +++ b/tauri_app/docker/patch-android-signing.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Подключает release-подпись APK через keystore.properties + Gradle script. +set -euo pipefail + +ANDROID_GEN="${1:-/workspace/tauri_app/src-tauri/gen/android}" +SECRETS_DIR="${ANDROID_SECRETS_DIR:-/workspace/tauri_app/.secrets}" +KEYSTORE_FILE="${ANDROID_KEYSTORE_FILE:-${SECRETS_DIR}/lendry-id-release.keystore}" +APP_GRADLE="${ANDROID_GEN}/app/build.gradle.kts" +KEYSTORE_PROPS="${ANDROID_GEN}/keystore.properties" +SIGNING_SCRIPT="${ANDROID_GEN}/app/release-signing.gradle.kts" +PROPS_TEMPLATE="${SECRETS_DIR}/keystore.properties.template" + +if [[ ! -d "${ANDROID_GEN}" ]]; then + echo "Android project not found: ${ANDROID_GEN}" >&2 + exit 1 +fi + +if [[ ! -f "${KEYSTORE_FILE}" ]]; then + echo "Keystore не найден: ${KEYSTORE_FILE}. Запустите ensure-android-keystore.sh" >&2 + exit 1 +fi + +if [[ -f "${PROPS_TEMPLATE}" ]]; then + cp "${PROPS_TEMPLATE}" "${KEYSTORE_PROPS}" +else + echo "keystore.properties.template не найден в ${SECRETS_DIR}" >&2 + exit 1 +fi + +chmod 600 "${KEYSTORE_PROPS}" || true + +cat > "${SIGNING_SCRIPT}" <<'EOF' +import com.android.build.api.dsl.ApplicationExtension +import java.util.Properties + +val keystoreProperties = Properties().apply { + rootProject.file("keystore.properties").inputStream().use { load(it) } +} + +extensions.configure("android") { + signingConfigs { + create("release") { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = rootProject.file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + } + } + buildTypes { + getByName("release") { + signingConfig = signingConfigs.getByName("release") + } + } +} +EOF + +if ! grep -q 'release-signing.gradle.kts' "${APP_GRADLE}"; then + printf '\napply(from = "release-signing.gradle.kts")\n' >> "${APP_GRADLE}" +fi + +echo "==> Android release signing patched (${KEYSTORE_PROPS})" diff --git a/tauri_app/docker/verify-apk-signature.sh b/tauri_app/docker/verify-apk-signature.sh new file mode 100644 index 0000000..8d2d742 --- /dev/null +++ b/tauri_app/docker/verify-apk-signature.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +APK_PATH="${1:-}" +ANDROID_HOME="${ANDROID_HOME:-/opt/android-sdk}" + +if [[ -z "${APK_PATH}" || ! -f "${APK_PATH}" ]]; then + echo "APK не найден для проверки подписи: ${APK_PATH}" >&2 + exit 1 +fi + +APKSIGNER="$(find "${ANDROID_HOME}/build-tools" -name apksigner -type f 2>/dev/null | sort -V | tail -n 1 || true)" + +if [[ -z "${APKSIGNER}" ]]; then + echo "apksigner не найден в ${ANDROID_HOME}/build-tools" >&2 + exit 1 +fi + +echo "==> Проверка подписи APK: ${APK_PATH}" +"${APKSIGNER}" verify --verbose "${APK_PATH}" +echo "==> Подпись APK подтверждена (apksigner verify)"