""" This file is part of the SplendidBear Websites' projects. Copyright (c) 2026 @ www.splendidbear.org For the full copyright and license information, please view the LICENSE file that was distributed with this source code. """ from __future__ import annotations import threading from collections.abc import Callable import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, GLib from mineseeker.api.auth import submit_totp, AuthError class TotpPage(Gtk.Box): """6-digit TOTP code entry shown after a successful password login.""" def __init__( self, on_success: Callable[[], None], on_back: Callable[[], None], ) -> None: super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0) self._on_success = on_success self._on_back = on_back self.set_valign(Gtk.Align.CENTER) self.set_halign(Gtk.Align.CENTER) clamp = Adw.Clamp() clamp.set_maximum_size(360) inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) inner.set_margin_top(32) inner.set_margin_bottom(32) inner.set_margin_start(16) inner.set_margin_end(16) title = Gtk.Label(label="Two-Factor Authentication") title.add_css_class("title-2") inner.append(title) subtitle = Gtk.Label(label="Enter the 6-digit code from your authenticator app.") subtitle.set_wrap(True) subtitle.add_css_class("dim-label") inner.append(subtitle) group = Adw.PreferencesGroup() self._code_row = Adw.EntryRow(title="Authentication Code") self._code_row.set_input_purpose(Gtk.InputPurpose.DIGITS) self._code_row.connect("entry-activated", self._on_verify_clicked) group.add(self._code_row) inner.append(group) self._error_label = Gtk.Label(label="") self._error_label.add_css_class("error") self._error_label.set_visible(False) inner.append(self._error_label) self._verify_btn = Gtk.Button(label="Verify") self._verify_btn.add_css_class("suggested-action") self._verify_btn.add_css_class("pill") self._verify_btn.connect("clicked", self._on_verify_clicked) inner.append(self._verify_btn) back_btn = Gtk.Button(label="Back to Login") back_btn.add_css_class("pill") back_btn.connect("clicked", lambda *_: self._on_back()) inner.append(back_btn) clamp.set_child(inner) self.append(clamp) def _set_busy(self, busy: bool) -> None: self._verify_btn.set_sensitive(not busy) self._code_row.set_sensitive(not busy) if busy: self._error_label.set_visible(False) def _show_error(self, message: str) -> None: self._error_label.set_label(message) self._error_label.set_visible(True) def _on_verify_clicked(self, *_) -> None: code = self._code_row.get_text().strip() if len(code) != 6 or not code.isdigit(): self._show_error("Code must be exactly 6 digits.") return self._set_busy(True) threading.Thread(target=self._do_verify, args=(code,), daemon=True).start() def _do_verify(self, code: str) -> None: try: submit_totp(code) GLib.idle_add(self._on_success) except AuthError as e: GLib.idle_add(self._handle_error, str(e)) except Exception as e: GLib.idle_add(self._handle_error, f"Connection error: {e}") def _handle_error(self, message: str) -> bool: self._set_busy(False) self._show_error(message) return GLib.SOURCE_REMOVE