update QML Captha

This commit is contained in:
dranik
2026-05-04 13:20:07 +03:00
parent 083f5b19ba
commit 344e7106c9
4 changed files with 207 additions and 108 deletions

View File

@@ -286,7 +286,7 @@ bool SubscriptionUiController::importFreeFromGateway()
m_captchaState.isPending = true;
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
captchaInfo.hint.isEmpty() ? tr("Please solve the CAPTCHA to continue") : captchaInfo.hint);
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
return false;
} else {
emit errorOccurred(errorCode);
@@ -351,7 +351,7 @@ void SubscriptionUiController::onRefreshCaptchaRequested()
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
captchaInfo.hint.isEmpty() ? tr("Please solve the CAPTCHA to continue") : captchaInfo.hint);
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
} else if (errorCode != ErrorCode::NoError) {
m_captchaState.isPending = false;
emit errorOccurred(errorCode);

View File

@@ -1,9 +1,12 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import Qt5Compat.GraphicalEffects
import Style 1.0
import "."
import "TextTypes"
import "../Config"
@@ -12,7 +15,7 @@ Popup {
property string captchaId
property string captchaImageBase64
property string hint: "Please solve the CAPTCHA to continue"
property string hint: qsTr("Enter the digits from the image to continue")
signal captchaSolved(string captchaId, string solution)
signal refreshCaptchaRequested()
@@ -33,8 +36,8 @@ Popup {
onOpened: {
timer.start()
solutionInput.text = ""
solutionInput.focus = true
solutionField.textField.text = ""
solutionField.textField.focus = true
}
onClosed: {
@@ -43,8 +46,8 @@ Popup {
background: Rectangle {
anchors.fill: parent
color: "white"
radius: 4
color: AmneziaStyle.color.slateGray
radius: 22
}
Timer {
@@ -52,7 +55,7 @@ Popup {
interval: 200
onTriggered: {
FocusController.pushRootObject(root)
FocusController.setFocusItem(solutionInput)
FocusController.setFocusItem(solutionField.textField)
}
repeat: false
running: true
@@ -68,35 +71,47 @@ Popup {
id: contentLayout
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.topMargin: 16
anchors.bottomMargin: 16
anchors.leftMargin: 20
anchors.rightMargin: 20
anchors.topMargin: 20
anchors.bottomMargin: 20
spacing: 12
spacing: 16
Text {
id: titleText
CaptionTextType {
text: qsTr("CAPTCHA Verification")
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
}
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
ParagraphTextType {
text: hint
Layout.fillWidth: true
text: root.hint
wrapMode: Text.WordWrap
color: AmneziaStyle.color.paleGray
font.pixelSize: 18
font.weight: Font.Bold
font.family: "PT Root UI VF"
lineHeight: 24 + LanguageUiController.getLineHeightAppend()
lineHeightMode: Text.FixedHeight
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignTop
}
Rectangle {
Item {
Layout.fillWidth: true
Layout.preferredHeight: 200
color: AmneziaStyle.color.lightGray
radius: 4
Rectangle {
id: imagePanel
anchors.fill: parent
color: AmneziaStyle.color.pearlGray
radius: 16
Image {
id: captchaImage
anchors.centerIn: parent
fillMode: Image.PreserveAspectFit
cache: false
Component.onCompleted: {
@@ -117,91 +132,120 @@ Popup {
anchors.centerIn: parent
running: captchaImage.status === Image.Loading
}
Rectangle {
id: refreshHit
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 10
width: 44
height: 44
radius: width / 2
color: AmneziaStyle.color.charcoalGray
Image {
id: refreshIcon
anchors.centerIn: parent
width: 26
height: 26
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: true
antialiasing: true
source: "qrc:/images/controls/refresh-cw.svg"
// Rasterize SVG at high resolution, then scale down — avoids blocky edges on HiDPI.
readonly property real _dpr: (Window.window && Window.window.screen)
? Window.window.screen.devicePixelRatio : 2.0
readonly property int _raster: Math.ceil(64 * Math.min(Math.max(_dpr, 1.0), 4.0))
sourceSize: Qt.size(_raster, _raster)
layer.enabled: true
layer.smooth: true
layer.textureSize: Qt.size(_raster, _raster)
layer.effect: ColorOverlay {
color: AmneziaStyle.color.goldenApricot
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.refreshCaptchaRequested()
}
}
}
}
TextFieldWithHeaderType {
id: solutionField
ParagraphTextType {
text: qsTr("Can't read the image?")
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
Layout.alignment: Qt.AlignLeft
headerText: qsTr("Digits from the image")
headerTextColor: AmneziaStyle.color.mutedGray
textField.placeholderText: qsTr("_ _ _ _ _ _")
textField.placeholderTextColor: AmneziaStyle.color.mutedGray
textField.inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoPredictiveText
textField.maximumLength: 6
textField.font.letterSpacing: 2
textField.onAccepted: {
submitIfNonEmpty()
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 8
}
BasicButtonType {
text: qsTr("Refresh")
implicitHeight: 32
onClicked: {
root.refreshCaptchaRequested()
}
}
}
ParagraphTextType {
text: qsTr("Enter the numbers from the image:")
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
}
TextField {
id: solutionInput
id: continueButton
Layout.fillWidth: true
implicitHeight: 40
implicitHeight: 52
placeholderText: qsTr("Enter CAPTCHA solution")
background: Rectangle {
border.color: AmneziaStyle.color.charcoalGray
border.width: 1
radius: 4
color: "white"
}
onAccepted: {
if (solutionInput.text.trim() !== "") {
root.captchaSolved(root.captchaId, solutionInput.text.trim())
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: 8
BasicButtonType {
id: submitButton
Layout.fillWidth: true
implicitHeight: 40
text: qsTr("Submit")
onClicked: {
if (solutionInput.text.trim() !== "") {
root.captchaSolved(root.captchaId, solutionInput.text.trim())
}
}
}
BasicButtonType {
id: cancelButton
Layout.fillWidth: true
implicitHeight: 40
text: qsTr("Cancel")
defaultColor: AmneziaStyle.color.lightGray
hoveredColor: AmneziaStyle.color.charcoalGray
text: qsTr("Continue")
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
onClicked: {
clickedFunc: function() {
submitIfNonEmpty()
}
}
BasicButtonType {
id: closeButton
Layout.fillWidth: true
implicitHeight: 52
text: qsTr("Close")
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.paleGray
borderWidth: 1
borderColor: AmneziaStyle.color.mutedGray
borderFocusedColor: AmneziaStyle.color.paleGray
clickedFunc: function() {
root.close()
}
}
}
}
function submitIfNonEmpty() {
const t = solutionField.textField.text.trim()
if (t !== "") {
root.captchaSolved(root.captchaId, t)
}
}
}

View File

@@ -0,0 +1,55 @@
# Local gateway (plaintext mock)
Минимальный HTTP-сервер на Go, который имитирует ответы Amnezia API gateway **без шифрования**: те же JSON-тела, что клиент отправляет в зашифрованном виде на прод. Удобно для отладки UI (в том числе CAPTCHA) и сценария **Amnezia Free**.
## Требования
- [Go](https://go.dev/dl/) **1.21** или новее (см. `go.mod`).
## Запуск
Из каталога `tools/local_gateway`:
```bash
cd tools/local_gateway
go mod download
go run .
```
Сервер слушает **`http://127.0.0.1:8080`** (в коде задано явно).
В логах должно появиться сообщение вида:
`plaintext mock listening on :8080 POST /v1/services POST /v1/config`
## Эндпоинты
| Метод | Путь | Назначение |
|--------|------|------------|
| `POST` | `/v1/services` | Минимальный ответ со списком сервисов (в т.ч. `amnezia-free` / `awg`). |
| `POST` | `/v1/config` | Импорт конфига: лимит/CAPTCHA (`dchest/captcha`), проверка решения, мок-ответы. |
Других маршрутов нет.
## Связка с клиентом AmneziaVPN
1. Соберите клиент с флагом CMake **`AMNEZIA_LOCAL_GATEWAY=ON`** — тогда для `localhost` запросы к gateway уходят **plaintext JSON** без RSA/AES (см. `GatewayController`, `SecureAppSettingsRepository`).
2. В настройках приложения endpoint gateway должен указывать на **`http://localhost:8080/`** (или `http://127.0.0.1:8080/`). При включённом `AMNEZIA_LOCAL_GATEWAY` дефолтный URL в коде уже `http://localhost:8080/`.
После этого сценарии вроде **Amnezia Free → Continue** будут ходить в этот mock.
## Поведение CAPTCHA (для разработчика)
В `main.go` константа **`rateLimitExcessAfter`**: при `0` «лимит» срабатывает сразу и первый запрос к `/v1/config` для `amnezia-free` чаще возвращает ответ с CAPTCHA; большее значение имитирует N успешных запросов до CAPTCHA.
Опционально в теле `POST /v1/config` mock обрабатывает **`refresh_captcha": true`** (отдельная ветка в коде); кнопка «Обновить» в клиенте может повторно вызывать обычный импорт без этого поля — смотрите актуальную логику в `SubscriptionUiController`.
## Зависимости
- `github.com/dchest/captcha` — генерация и проверка картинки CAPTCHA.
После изменения зависимостей:
```bash
go mod tidy
```

View File

@@ -174,7 +174,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
"error": "rate_limit_exceeded",
"captcha_id": id,
"captcha_image": b64,
"hint": "Please solve the CAPTCHA to continue",
"hint": "Enter the digits from the image to continue",
})
return
}