From 56bb59915dc31ec15a03306b9c41a133dff75f8f Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Sun, 17 May 2026 22:33:40 +0200 Subject: [PATCH] feat(debug,protection): Force Reset for Android screenshot-capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug-context: user reports nach Cooldown-Disable auf v0.2.1 Android-Build reactiviert sich Schutz auto → a11y-Settings bleibt blockiert → keine Screenshots möglich. v0.3.0 hat den Backend-protectionDisabledAt-Guard der das verhindert, aber Test-Devices brauchen ein direktes Reset-Tool für Multi-Locale-Screenshots. Backend: - POST /api/protection/dev-force-disabled — sets protectionDisabledAt=NOW() ohne Cooldown-Vorlauf. Production-Guard (rebreak.org-non-staging → 403). Frontend: - /debug Android-Section refactored: "Force Reset + Settings öffnen" Button - Bundle aus 3 Steps: 1. native forceDisable (VPN stop + tamper disarm + filter_enabled=false) 2. backend dev-force-disabled (Anti-Auto-Reactivation-Mark) 3. Settings → Bedienungshilfen öffnen - Danach: User toggled ReBreak-Service in Android-Settings manuell off → frischer a11y-deep-link-Trigger für nächste Screenshot-Iteration Also: fix /onboarding/welcome → /onboarding (Duo-Rewrite hat den alten Pfad gelöscht). Route 404 auf Android sichtbar wenn User in debug-toggle 'welcome' oder 'nickname' tappt. Co-Authored-By: Claude Opus 4.7 --- apps/rebreak-native/app/debug.tsx | 124 +++++++++++++++++- .../api/protection/dev-force-disabled.post.ts | 37 ++++++ 2 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 backend/server/api/protection/dev-force-disabled.post.ts diff --git a/apps/rebreak-native/app/debug.tsx b/apps/rebreak-native/app/debug.tsx index fc9a9d1..7e1e930 100644 --- a/apps/rebreak-native/app/debug.tsx +++ b/apps/rebreak-native/app/debug.tsx @@ -7,6 +7,7 @@ import { TouchableOpacity, Alert, Clipboard, + Platform, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; @@ -15,7 +16,7 @@ import { useColors } from '../lib/theme'; import { useMe, invalidateMe, type Plan } from '../hooks/useMe'; import { apiFetch } from '../lib/api'; import { PlanChangeSheet } from '../components/plan/PlanChangeSheet'; -import { getCooldownTestMode, setCooldownTestMode } from '../lib/protection'; +import { getCooldownTestMode, setCooldownTestMode, protection } from '../lib/protection'; import { useRealtimeDebugStore, type LogEntry } from '../stores/realtimeDebug'; import { supabase } from '../lib/supabase'; @@ -115,6 +116,8 @@ export default function DebugScreen() { + {Platform.OS === 'android' ? : null} + @@ -690,9 +693,13 @@ function OnboardingResetToggle({ }); invalidateMe(); if (step === 'welcome') { - router.replace('/onboarding/welcome'); + // /onboarding/welcome existiert nicht mehr (Duo-Rewrite → /onboarding/index.tsx). + // Korrekter Pfad ist /onboarding (Expo Router löst index.tsx auto auf). + router.replace('/onboarding'); } else if (step === 'nickname') { - router.replace('/profile/edit'); + // Legacy-Stage — Duo-Flow navigiert intern; /onboarding triggert via + // slideFromStep den Resume zur Nickname-Slide. + router.replace('/onboarding'); } else if (step === 'block') { router.replace('/(app)/blocker'); } else if (step === 'done') { @@ -783,6 +790,117 @@ function OnboardingResetToggle({ ); } +// ─── Android: A11y-Settings Quick-Open ────────────────────────────────────── + +/** + * Android-only Dev-Helper: öffnet Settings → Bedienungshilfen damit User den + * ReBreak-Service manuell off-toggeln kann. Wird gebraucht für Screenshot- + * Capture vom a11y-Settings-Page in 4 Sprachen — nach Cooldown disarmt + * forceDisable() den tamper-lock, aber das System-Switch in den Einstellungen + * bleibt programmatisch un-modifizierbar (Android-OS-Restriction). + */ +function AndroidA11yResetToggle({ + colors, +}: { + colors: import('../lib/theme').ColorScheme; +}) { + const [busy, setBusy] = useState(false); + + async function forceResetAndOpen() { + if (busy) return; + setBusy(true); + try { + // Step 1: native forceDisable — stoppt VPN, disarmt tamper-lock, setzt + // filter_enabled=false → a11y-service wird passiv → blockt nichts mehr. + await protection.forceDisable(); + + // Step 2: backend protection-state auf "explizit disabled" markieren → + // enforceProtection-Loop feuert KEINE Auto-Reactivation in den nächsten + // Pollings. Sonst wäre der a11y-Service-Off-Toggle zwecklos weil die + // App den Filter sofort wieder hochfährt. + await apiFetch('/api/protection/dev-force-disabled', { method: 'POST' }); + invalidateMe(); + + // Step 3: Android-Settings → Bedienungshilfen öffnen — User toggelt + // ReBreak-Service manuell off (Android-OS-Restriction: programmatisch + // nicht setzbar). Danach Screenshot-Capture frischer Trigger möglich. + await protection.openSystemSettings('accessibility'); + } catch (e: unknown) { + Alert.alert('Fehler', e instanceof Error ? e.message : String(e)); + } finally { + setBusy(false); + } + } + + return ( + + + + + + + + Force Reset (Android) + + + VPN stoppen + tamper-lock disarmen + Backend als disabled markieren + + Settings öffnen — Anti-Auto-Reactivation für Screenshot-Capture. + + + + + + + {busy ? 'Reset läuft...' : 'Force Reset + Settings öffnen'} + + + + ); +} + // ─── Cooldown Test Mode ──────────────────────────────────────────────────── function CooldownTestModeToggle() { diff --git a/backend/server/api/protection/dev-force-disabled.post.ts b/backend/server/api/protection/dev-force-disabled.post.ts new file mode 100644 index 0000000..ba26815 --- /dev/null +++ b/backend/server/api/protection/dev-force-disabled.post.ts @@ -0,0 +1,37 @@ +import { requireUser } from "../../utils/auth"; +import { usePrisma } from "../../utils/prisma"; + +/** + * POST /api/protection/dev-force-disabled + * + * DEV/STAGING-ONLY: Setzt protectionDisabledAt = NOW() ohne Cooldown-Vorlauf. + * Frontend-Debug-Button für Screenshot-Capture (Android-a11y-reset-flow). + * + * Production-Guard: appUrl enthält "rebreak.org" aber NICHT "staging" → 403. + * + * Sobald gesetzt: + * - /api/protection/state gibt protectionShouldBeActive=false zurück + * - Frontend's enforceProtection-Loop feuert KEINE Auto-Reactivation mehr + * - User kann a11y-Settings öffnen und manuell den ReBreak-Service off-toggeln + * + * Zum Wiedereinschalten: POST /api/protection/mark-active (clear flag). + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + + const config = useRuntimeConfig(event); + const appUrl = (config.public?.appUrl as string) ?? ""; + const isProductionUrl = + appUrl.includes("rebreak.org") && !appUrl.includes("staging"); + if (isProductionUrl) { + throw createError({ statusCode: 403, message: "dev-only" }); + } + + const db = usePrisma(); + await db.profile.update({ + where: { id: user.id }, + data: { protectionDisabledAt: new Date() }, + }); + + return { success: true }; +});