alias Towerops.Accounts # The code from the error provided_code = "147273" server_time = 1_769_699_437 IO.puts("=== Searching for when code #{provided_code} would be valid ===") IO.puts("Server time: #{DateTime.from_unix!(server_time)}") IO.puts("") # Generate a fresh secret to test with secret = Accounts.generate_totp_secret() IO.puts("Testing with a fresh 20-byte secret...") IO.puts("") # Search ±1 hour (120 time steps of 30 seconds each) found = Enum.find_value(-120..120, fn offset -> test_time = server_time + offset * 30 expected = NimbleTOTP.verification_code(secret, time: test_time) if expected == provided_code do %{ offset_seconds: offset * 30, offset_minutes: Float.round(offset * 30 / 60, 1), time: DateTime.from_unix!(test_time) } end end) case found do nil -> IO.puts("❌ Code #{provided_code} would NEVER be valid for a fresh 20-byte secret") IO.puts("") IO.puts("This means:") IO.puts(" 1. Your authenticator app has a DIFFERENT secret than the server") IO.puts(" 2. You need to:") IO.puts(" - Delete the Towerops entry from your authenticator app") IO.puts(" - Visit the enrollment page again (fresh)") IO.puts(" - Scan the NEW QR code") IO.puts(" - Enter the code immediately") match -> IO.puts("✓ Code #{provided_code} would be valid at:") IO.puts(" Time: #{match.time}") IO.puts(" Offset: #{match.offset_seconds} seconds (#{match.offset_minutes} minutes)") IO.puts("") cond do abs(match.offset_seconds) <= 120 -> IO.puts("⚠️ This is within the ±2 minute window - should have worked!") abs(match.offset_seconds) <= 3600 -> IO.puts("⏰ Clock drift: #{abs(match.offset_minutes)} minutes") IO.puts(" Your device's clock is incorrect") true -> IO.puts("⏰ Extreme time difference: #{abs(match.offset_minutes)} minutes") IO.puts(" Your device's clock is way off") end end