From 9e4c43d40122506051d533adfbfda36c545afa2d Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 7 May 2025 17:03:49 +0200 Subject: [PATCH 01/17] Cleanup of permission utils from un neccessary objects - remove 4113 from download apis --- server/mergin/sync/private_api.yaml | 4 -- .../lib/src/common/permission_utils.ts | 72 ++++--------------- 2 files changed, 14 insertions(+), 62 deletions(-) diff --git a/server/mergin/sync/private_api.yaml b/server/mergin/sync/private_api.yaml index 8c7c8491..389a6d7b 100644 --- a/server/mergin/sync/private_api.yaml +++ b/server/mergin/sync/private_api.yaml @@ -410,8 +410,6 @@ paths: description: Accepted "400": $ref: "#/components/responses/BadStatusResp" - "413": - $ref: "#/components/responses/FileTooLargeResp" "403": $ref: "#/components/responses/Forbidden" "404": @@ -425,8 +423,6 @@ components: description: Project not found. BadStatusResp: description: Invalid request. - FileTooLargeResp: - description: File is too large. InvalidDataResp: description: Invalid/unprocessable data. Success: diff --git a/web-app/packages/lib/src/common/permission_utils.ts b/web-app/packages/lib/src/common/permission_utils.ts index be94ba8c..6905f90e 100644 --- a/web-app/packages/lib/src/common/permission_utils.ts +++ b/web-app/packages/lib/src/common/permission_utils.ts @@ -28,20 +28,18 @@ export enum GlobalRole { global_admin } -export type WorkspaceRoleName = - | 'guest' - | 'reader' - | 'editor' - | 'writer' - | 'admin' - | 'owner' +export enum ProjectPermission { + read, + edit, + write, + owner +} -export type ProjectRoleName = Extract< - WorkspaceRoleName, - 'reader' | 'editor' | 'writer' | 'owner' -> +export type WorkspaceRoleName = keyof typeof WorkspaceRole -export type ProjectPermissionName = 'owner' | 'write' | 'edit' | 'read' +export type ProjectRoleName = keyof typeof ProjectRole + +export type ProjectPermissionName = keyof typeof ProjectPermission export const USER_ROLE_NAME_BY_ROLE: Record = { @@ -53,15 +51,6 @@ export const USER_ROLE_NAME_BY_ROLE: Record = [WorkspaceRole.owner]: 'owner' } -export const USER_ROLE_BY_NAME: Record = { - guest: WorkspaceRole.guest, - reader: WorkspaceRole.reader, - editor: WorkspaceRole.editor, - writer: WorkspaceRole.writer, - admin: WorkspaceRole.admin, - owner: WorkspaceRole.owner -} - export const PROJECT_ROLE_NAME_BY_ROLE: Record = { [ProjectRole.reader]: 'reader', [ProjectRole.editor]: 'editor', @@ -69,59 +58,26 @@ export const PROJECT_ROLE_NAME_BY_ROLE: Record = { [ProjectRole.owner]: 'owner' } -export const PROJECT_ROLE_BY_NAME: Record = { - reader: ProjectRole.reader, - editor: ProjectRole.editor, - writer: ProjectRole.writer, - owner: ProjectRole.owner -} - -export enum ProjectPermission { - read, - edit, - write, - owner -} - -export const PROJECT_PERMISSION_NAME_BY_PERMISSION: Record< - ProjectPermission, - ProjectPermissionName -> = { - [ProjectPermission.read]: 'read', - [ProjectPermission.edit]: 'edit', - [ProjectPermission.write]: 'write', - [ProjectPermission.owner]: 'owner' -} - -export const PROJECT_PERMISSION_BY_NAME: Record< - ProjectPermissionName, - ProjectPermission -> = { - read: ProjectPermission.read, - edit: ProjectPermission.edit, - write: ProjectPermission.write, - owner: ProjectPermission.owner -} export function isAtLeastRole( roleName: WorkspaceRoleName, role: WorkspaceRole ): boolean { - return USER_ROLE_BY_NAME[roleName] >= role + return WorkspaceRole[roleName] >= role } export function isAtLeastProjectRole( roleName: ProjectRoleName, role: ProjectRole ): boolean { - return PROJECT_ROLE_BY_NAME[roleName] >= role + return ProjectRole[roleName] >= role } export function isAtLeastProjectPermission( permissionName: ProjectPermissionName, permission: ProjectPermission ): boolean { - return PROJECT_PERMISSION_BY_NAME[permissionName] >= permission + return ProjectPermission[permissionName] >= permission } export function isAtLeastGlobalRole( @@ -133,7 +89,7 @@ export function isAtLeastGlobalRole( [GlobalRole.global_write]: ProjectRole.writer, [GlobalRole.global_admin]: ProjectRole.owner } - return PROJECT_ROLE_BY_NAME[roleName] >= globalProjectRole[globalRole] + return ProjectRole[roleName] >= globalProjectRole[globalRole] } export function getProjectRoleNameValues(): DropdownOption[] { From e6832729178151e1aaeb88a17795a205422eaf40 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Fri, 9 May 2025 12:30:49 +0200 Subject: [PATCH 02/17] Hide receive notifications for can_edit_profile users false --- .../lib/src/modules/user/views/ProfileViewTemplate.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue b/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue index 7326f055..12358d8c 100644 --- a/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue +++ b/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial

Account details

Advanced
Date: Fri, 9 May 2025 17:00:42 +0200 Subject: [PATCH 03/17] Initial version for diagnostic logs: - add new v2 endpoint to stats module - save file to DIAGNOSTIC_LOGS_URL - propagate to config --- deployment/community/.env.template | 4 ++ server/.test.env | 1 + server/application.py | 1 + server/mergin/config.py | 4 ++ server/mergin/stats/api.yaml | 40 +++++++++++++++++-- server/mergin/stats/config.py | 10 +++++ server/mergin/stats/controller.py | 31 ++++++++++++++- server/mergin/stats/utils.py | 21 ++++++++++ server/mergin/tests/fixtures.py | 4 ++ server/mergin/tests/test_statistics.py | 41 +++++++++++++++++++- server/mergin/tests/test_statistics_utils.py | 37 ++++++++++++++++++ 11 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 server/mergin/stats/utils.py create mode 100644 server/mergin/tests/test_statistics_utils.py diff --git a/deployment/community/.env.template b/deployment/community/.env.template index 741e0e9c..ce910efd 100644 --- a/deployment/community/.env.template +++ b/deployment/community/.env.template @@ -208,3 +208,7 @@ GEVENT_WORKER=True # Deprecated from 2024.7.0, replacement is to set GEVENT_WORKER=True NO_MONKEY_PATCH=False +# Diagnostic logs + +DIAGNOSTIC_LOGS_DIR=/diagnostic_logs + diff --git a/server/.test.env b/server/.test.env index 908db5b9..bdaa7bfa 100644 --- a/server/.test.env +++ b/server/.test.env @@ -23,3 +23,4 @@ GEODIFF_WORKING_DIR=/tmp/geodiff SECURITY_BEARER_SALT='bearer' SECURITY_EMAIL_SALT='email' SECURITY_PASSWORD_SALT='password' +DIAGNOSTIC_LOGS_DIR=/tmp/diagnostic_logs diff --git a/server/application.py b/server/application.py index 5cb08b69..b1ab79ac 100644 --- a/server/application.py +++ b/server/application.py @@ -46,6 +46,7 @@ "GLOBAL_READ", "GLOBAL_WRITE", "ENABLE_SUPERADMIN_ASSIGNMENT", + "DIAGNOSTIC_LOGS_URL", ] ) register_stats(application) diff --git a/server/mergin/config.py b/server/mergin/config.py index f633e171..f54c39cc 100644 --- a/server/mergin/config.py +++ b/server/mergin/config.py @@ -107,3 +107,7 @@ class Configuration(object): # using gevent type of worker impose some requirements on code, e.g. to be greenlet safe, custom timeouts GEVENT_WORKER = config("GEVENT_WORKER", default=False, cast=bool) GEVENT_REQUEST_TIMEOUT = config("GEVENT_REQUEST_TIMEOUT", default=30, cast=int) + DIAGNOSTIC_LOGS_URL = config( + "DIAGNOSTIC_LOGS_URL", + default="", + ) diff --git a/server/mergin/stats/api.yaml b/server/mergin/stats/api.yaml index 382da567..e1a5d492 100644 --- a/server/mergin/stats/api.yaml +++ b/server/mergin/stats/api.yaml @@ -18,7 +18,7 @@ paths: schema: $ref: "#/components/schemas/ServerVersion" "400": - $ref: "#/components/responses/BadStatusResp" + $ref: "#/components/responses/BadRequestResp" "404": $ref: "#/components/responses/NotFoundResp" x-openapi-router-controller: mergin.stats.controller @@ -50,17 +50,51 @@ paths: schema: type: string "400": - $ref: "#/components/responses/BadStatusResp" + $ref: "#/components/responses/BadRequestResp" "404": $ref: "#/components/responses/NotFoundResp" + /v2/diagnostic-logs: + post: + summary: Save diagnostic logs + operationId: save_diagnostic_log + x-openapi-router-controller: mergin.stats.controller + parameters: + - name: app + in: query + description: Application name (e.g., "input-android-0.9.0") + required: true + schema: + type: string + - name: username + in: query + description: Username + required: true + schema: + type: string + requestBody: + required: true + content: + text/plain: + schema: + type: string + description: Log content in plain text + responses: + "200": + description: Log saved successfully + "400": + $ref: "#/components/responses/BadRequestResp" + "413": + $ref: "#/components/responses/RequestTooLarge" components: responses: UnauthorizedError: description: Authentication information is missing or invalid. NotFoundResp: description: Project not found. - BadStatusResp: + BadRequestResp: description: Invalid request. + RequestTooLarge: + description: Request Entity Too Large. schemas: ServerVersion: type: object diff --git a/server/mergin/stats/config.py b/server/mergin/stats/config.py index 795df286..e251e851 100644 --- a/server/mergin/stats/config.py +++ b/server/mergin/stats/config.py @@ -2,9 +2,13 @@ # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial +import os from decouple import config +config_dir = os.path.abspath(os.path.dirname(__file__)) + + class Configuration(object): # send statistics about usage COLLECT_STATISTICS = config("COLLECT_STATISTICS", default=True, cast=bool) @@ -16,3 +20,9 @@ class Configuration(object): STATISTICS_URL = config( "STATISTICS_URL", default="https://api.merginmaps.com/monitoring/v1" ).rstrip("/") + DIAGNOSTIC_LOGS_DIR = config( + "DIAGNOSTIC_LOGS_DIR", + default=os.path.join( + config_dir, os.pardir, os.pardir, os.pardir, "diagnostic_logs" + ), + ) diff --git a/server/mergin/stats/controller.py b/server/mergin/stats/controller.py index 4606f1e5..87fd6f34 100644 --- a/server/mergin/stats/controller.py +++ b/server/mergin/stats/controller.py @@ -2,14 +2,15 @@ # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial -from dataclasses import asdict import requests -from flask import abort, current_app, make_response +from flask import abort, current_app, make_response, request from datetime import datetime, time from csv import DictWriter +from pathvalidate import sanitize_filename from mergin.auth.app import auth_required from mergin.stats.models import MerginStatistics, ServerCallhomeData +from mergin.stats.utils import save_diagnostic_log_file from .config import Configuration from ..app import parse_version_string, db @@ -88,3 +89,29 @@ def download_report(date_from: str, date_to: str): response.headers["Content-Disposition"] = f"attachment; filename=usage-report.csv" response.mimetype = "text/csv" return response + + +def save_diagnostic_log(app: str, username: str): + """Save diagnostic logs""" + # if server is using external storage, we don't want to save logs + if app.config.get("DIAGNOSTIC_LOGS_URL"): + abort(404) + + # check if plain text body is not larger than 1MB + max_size = 1024 * 1024 + if request.content_length > max_size: + abort(413) + # get body from request + body = request.get_data() + if not body: + abort(400) + if len(body) > max_size: + abort(413) + + # save diagnostic log file + folder = current_app.config.get("DIAGNOSTIC_LOGS_DIR") + save_diagnostic_log_file( + app, username, body, current_app.config["DIAGNOSTIC_LOGS_DIR"] + ) + + return "Log saved successfully", 200 diff --git a/server/mergin/stats/utils.py b/server/mergin/stats/utils.py new file mode 100644 index 00000000..ae0e1155 --- /dev/null +++ b/server/mergin/stats/utils.py @@ -0,0 +1,21 @@ +from datetime import datetime, timezone +import os + +from pathvalidate import sanitize_filename + + +def save_diagnostic_log_file( + app: str, username: str, body: bytes, to_folder: str +) -> str: + """Save diagnostic log file to DIAGNOSTIC_LOGS_DIR""" + + content = body.decode("utf-8") + datetime_iso_str = datetime.now(tz=timezone.utc).isoformat() + file_name = sanitize_filename( + username + "_" + app + "_" + datetime_iso_str + ".log" + ) + os.makedirs(to_folder, exist_ok=True) + with open(os.path.join(to_folder, file_name), "w") as f: + f.write(content) + + return file_name diff --git a/server/mergin/tests/fixtures.py b/server/mergin/tests/fixtures.py index 6b83ecf9..7cff688e 100644 --- a/server/mergin/tests/fixtures.py +++ b/server/mergin/tests/fixtures.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial import os +import shutil import sys import uuid from copy import deepcopy @@ -80,6 +81,9 @@ def teardown(): if p.storage is not None ] cleanup(flask_app.test_client(), dirs) + diagnostic_logs_dir = flask_app.config.get("DIAGNOSTIC_LOGS_DIR") + if os.path.exists(diagnostic_logs_dir): + shutil.rmtree(diagnostic_logs_dir) request.addfinalizer(teardown) return flask_app diff --git a/server/mergin/tests/test_statistics.py b/server/mergin/tests/test_statistics.py index 6f339505..dc73ef96 100644 --- a/server/mergin/tests/test_statistics.py +++ b/server/mergin/tests/test_statistics.py @@ -2,11 +2,12 @@ # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial -import csv from dataclasses import asdict -from datetime import timedelta, timezone, datetime +from datetime import timezone, datetime import json +import os from unittest.mock import patch +from flask import url_for import requests from sqlalchemy.sql.operators import is_ @@ -220,3 +221,39 @@ def test_download_report(app, client): empty_file = f"{','.join(keys)}\r\n" assert resp.data.decode("UTF-8") == empty_file assert len(lines) == 1 + + +def test_save_diagnostic_log(client, app): + """Test save diagnostic log endpoint""" + url = url_for("/.mergin_stats_controller_save_diagnostic_log") + resp = client.post(url) + assert resp.status_code == 400 + + # bad request + resp = client.post(url, data="test") + assert resp.status_code == 400 + + url = url_for( + "/.mergin_stats_controller_save_diagnostic_log", + app="test_app", + username="test_user", + ) + + # too large request + resp = client.post(url, data="x" * (1024 * 1024 + 1)) + assert resp.status_code == 413 + + # valid request + resp = client.post(url, data="test") + assert resp.status_code == 200 + + # check if file was created + log_dir = app.config["DIAGNOSTIC_LOGS_DIR"] + assert os.path.exists(log_dir) + files = os.listdir(log_dir) + assert len(files) == 1 + assert files[0].startswith("test_user_test_app_") + assert files[0].endswith(".log") + with open(os.path.join(log_dir, files[0]), "r") as f: + content = f.read() + assert content == "test" diff --git a/server/mergin/tests/test_statistics_utils.py b/server/mergin/tests/test_statistics_utils.py new file mode 100644 index 00000000..fd83ebc2 --- /dev/null +++ b/server/mergin/tests/test_statistics_utils.py @@ -0,0 +1,37 @@ +from datetime import datetime, timezone +import os +from unittest.mock import patch + +from pathvalidate import sanitize_filename + +from ..stats.utils import save_diagnostic_log_file + + +def test_save_diagnostic_log_file(client, app): + """Test save diagnostic log file""" + # Mock datetime value + test_date = "2025-05-09T12:00:00+00:00" + app_name = "t" * 256 + username = "test-user" + body = b"Test log content" + to_folder = app.config["DIAGNOSTIC_LOGS_DIR"] + + saved_file_name = save_diagnostic_log_file(app_name, username, body, to_folder) + saved_file_path = os.path.join(to_folder, saved_file_name) + assert os.path.exists(saved_file_path) + assert len(saved_file_name) == 255 + + with patch("mergin.stats.utils.datetime") as mock_datetime: + mock_datetime.now.return_value = datetime.fromisoformat(test_date) + app_name = "test_<>app" + saved_file_name = save_diagnostic_log_file(app_name, username, body, to_folder) + # Check if the file was created + assert saved_file_name == sanitize_filename( + username + "_" + app_name + "_" + test_date + ".log" + ) + saved_file_path = os.path.join(to_folder, saved_file_name) + assert os.path.exists(saved_file_path) + # Check the content of the file + with open(saved_file_path, "r") as f: + content = f.read() + assert content == body.decode("utf-8") From 0079a0be1d9c64cc7d851fef5035f965f16e64f4 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Fri, 9 May 2025 17:22:10 +0200 Subject: [PATCH 04/17] fix config getting --- server/mergin/stats/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/mergin/stats/controller.py b/server/mergin/stats/controller.py index 87fd6f34..a0c38d67 100644 --- a/server/mergin/stats/controller.py +++ b/server/mergin/stats/controller.py @@ -94,7 +94,7 @@ def download_report(date_from: str, date_to: str): def save_diagnostic_log(app: str, username: str): """Save diagnostic logs""" # if server is using external storage, we don't want to save logs - if app.config.get("DIAGNOSTIC_LOGS_URL"): + if current_app.config.get("DIAGNOSTIC_LOGS_URL"): abort(404) # check if plain text body is not larger than 1MB @@ -111,7 +111,7 @@ def save_diagnostic_log(app: str, username: str): # save diagnostic log file folder = current_app.config.get("DIAGNOSTIC_LOGS_DIR") save_diagnostic_log_file( - app, username, body, current_app.config["DIAGNOSTIC_LOGS_DIR"] + app, username, body, current_app.config.get("DIAGNOSTIC_LOGS_DIR") ) return "Log saved successfully", 200 From 58ffbde8205c2384e7be550fcf7d2b5e57ff16f0 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 12 May 2025 09:31:47 +0200 Subject: [PATCH 05/17] small variable change --- server/mergin/stats/controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/mergin/stats/controller.py b/server/mergin/stats/controller.py index a0c38d67..57f9eb49 100644 --- a/server/mergin/stats/controller.py +++ b/server/mergin/stats/controller.py @@ -110,8 +110,6 @@ def save_diagnostic_log(app: str, username: str): # save diagnostic log file folder = current_app.config.get("DIAGNOSTIC_LOGS_DIR") - save_diagnostic_log_file( - app, username, body, current_app.config.get("DIAGNOSTIC_LOGS_DIR") - ) + save_diagnostic_log_file(app, username, body, folder) return "Log saved successfully", 200 From c6b3c2489c63b68728d660195350515bc121988b Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 12 May 2025 10:21:09 +0200 Subject: [PATCH 06/17] bump 2025.4.1 --- server/mergin/version.py | 2 +- server/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/mergin/version.py b/server/mergin/version.py index 1d779a6b..a87af32c 100644 --- a/server/mergin/version.py +++ b/server/mergin/version.py @@ -4,4 +4,4 @@ def get_version(): - return "2025.4.0" + return "2025.4.1" diff --git a/server/setup.py b/server/setup.py index 760468b6..db732ec7 100644 --- a/server/setup.py +++ b/server/setup.py @@ -6,7 +6,7 @@ setup( name="mergin", - version="2025.4.0", + version="2025.4.1", url="https://github.com/MerginMaps/mergin", license="AGPL-3.0-only", author="Lutra Consulting Limited", From 784f7c511416715d2cc5dd8bcdb9d4c20af68a6f Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 12 May 2025 12:46:55 +0200 Subject: [PATCH 07/17] Some text to run tests --- server/migrations/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/migrations/README b/server/migrations/README index 98e4f9c4..995a02e0 100644 --- a/server/migrations/README +++ b/server/migrations/README @@ -1 +1 @@ -Generic single-database configuration. \ No newline at end of file +Database alembic migrations for the project. From e4b29f6eeba5d3a03be97e8c31a66e929c1a6fb7 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 12 May 2025 13:58:07 +0200 Subject: [PATCH 08/17] add back 413 --- server/mergin/sync/private_api.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/mergin/sync/private_api.yaml b/server/mergin/sync/private_api.yaml index 389a6d7b..c50f9561 100644 --- a/server/mergin/sync/private_api.yaml +++ b/server/mergin/sync/private_api.yaml @@ -412,11 +412,15 @@ paths: $ref: "#/components/responses/BadStatusResp" "403": $ref: "#/components/responses/Forbidden" + "413": + $ref: "#/components/responses/FileTooLargeResp" "404": $ref: "#/components/responses/NotFoundResp" x-openapi-router-controller: mergin.sync.private_api_controller components: responses: + FileTooLargeResp: + description: File is too large. UnauthorizedError: description: Authentication information is missing or invalid. NotFoundResp: From ec756886e0824de56cbb56ad612ca8ab64de90ee Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 12 May 2025 14:15:36 +0200 Subject: [PATCH 09/17] Remove 413 --- server/mergin/sync/private_api.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/mergin/sync/private_api.yaml b/server/mergin/sync/private_api.yaml index c50f9561..389a6d7b 100644 --- a/server/mergin/sync/private_api.yaml +++ b/server/mergin/sync/private_api.yaml @@ -412,15 +412,11 @@ paths: $ref: "#/components/responses/BadStatusResp" "403": $ref: "#/components/responses/Forbidden" - "413": - $ref: "#/components/responses/FileTooLargeResp" "404": $ref: "#/components/responses/NotFoundResp" x-openapi-router-controller: mergin.sync.private_api_controller components: responses: - FileTooLargeResp: - description: File is too large. UnauthorizedError: description: Authentication information is missing or invalid. NotFoundResp: From c1f9133ffb0b1c23d9ab3de4c2275ff5ae0861e0 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 12 May 2025 14:35:12 +0200 Subject: [PATCH 10/17] change test --- server/mergin/sync/private_api.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/mergin/sync/private_api.yaml b/server/mergin/sync/private_api.yaml index 389a6d7b..cfcd6a8b 100644 --- a/server/mergin/sync/private_api.yaml +++ b/server/mergin/sync/private_api.yaml @@ -387,7 +387,7 @@ paths: get: tags: - project - summary: Download full project + summary: Download full project as zip file description: Download whole project folder as zip file operationId: download_project parameters: From d83e4a5faf10c8d1116023a304df63a187e69814 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 12 May 2025 14:47:49 +0200 Subject: [PATCH 11/17] just trying 413 replced by 422 (as it's real) --- server/mergin/sync/private_api.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/mergin/sync/private_api.yaml b/server/mergin/sync/private_api.yaml index cfcd6a8b..b5fa2c87 100644 --- a/server/mergin/sync/private_api.yaml +++ b/server/mergin/sync/private_api.yaml @@ -387,7 +387,7 @@ paths: get: tags: - project - summary: Download full project as zip file + summary: Download full project description: Download whole project folder as zip file operationId: download_project parameters: @@ -410,6 +410,8 @@ paths: description: Accepted "400": $ref: "#/components/responses/BadStatusResp" + "422": + $ref: "#/components/responses/UnprocessableEntity" "403": $ref: "#/components/responses/Forbidden" "404": @@ -423,6 +425,8 @@ components: description: Project not found. BadStatusResp: description: Invalid request. + UnprocessableEntity: + description: UnprocessableEntity InvalidDataResp: description: Invalid/unprocessable data. Success: From 4c5b6e28ea1d011ecc0a82814254dd6113cd7245 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Tue, 13 May 2025 16:22:36 +0200 Subject: [PATCH 12/17] Refactor: - move some endpoints to global app.py from stats - move some tests to `test_controller` Updated: - make max size configurable --- server/mergin/api.yaml | 84 ++++++++++++ server/mergin/app.py | 6 + server/mergin/config.py | 11 ++ server/mergin/controller.py | 61 +++++++++ server/mergin/stats/api.yaml | 71 ---------- server/mergin/stats/config.py | 9 -- server/mergin/stats/controller.py | 52 +------- server/mergin/stats/utils.py | 21 --- server/mergin/tests/test_controller.py | 131 +++++++++++++++++++ server/mergin/tests/test_statistics.py | 77 +---------- server/mergin/tests/test_statistics_utils.py | 37 ------ server/mergin/tests/test_utils.py | 68 +++++----- server/mergin/utils.py | 21 ++- 13 files changed, 352 insertions(+), 297 deletions(-) create mode 100644 server/mergin/api.yaml create mode 100644 server/mergin/controller.py delete mode 100644 server/mergin/stats/utils.py create mode 100644 server/mergin/tests/test_controller.py delete mode 100644 server/mergin/tests/test_statistics_utils.py diff --git a/server/mergin/api.yaml b/server/mergin/api.yaml new file mode 100644 index 00000000..6ebd2777 --- /dev/null +++ b/server/mergin/api.yaml @@ -0,0 +1,84 @@ +openapi: 3.0.0 +info: + description: Common Mergin Maps API + version: "0.1" + title: Common Mergin Maps API +servers: + - url: / +paths: + /v1/latest-version: + get: + summary: Fetch latest available server version + operationId: get_latest_version + responses: + "200": + description: Latest version info + content: + application/json: + schema: + $ref: "#/components/schemas/ServerVersion" + "400": + $ref: "#/components/responses/BadRequestResp" + "404": + $ref: "#/components/responses/NotFoundResp" + x-openapi-router-controller: mergin.controller + /v2/diagnostic-logs: + post: + summary: Save diagnostic log to the server + description: This endpoint allows users to upload diagnostic logs for troubleshooting purposes from mobile and plugin. + operationId: save_diagnostic_log + x-openapi-router-controller: mergin.controller + parameters: + - name: app + in: query + description: Application name (e.g., "input-android-0.9.0") + required: true + schema: + type: string + requestBody: + required: true + content: + text/plain: + schema: + type: string + description: Log content in plain text + responses: + "200": + description: Log saved successfully + "400": + $ref: "#/components/responses/BadRequestResp" + "404": + $ref: "#/components/responses/NotFoundResp" + "413": + $ref: "#/components/responses/RequestTooLarge" +components: + responses: + UnauthorizedError: + description: Authentication information is missing or invalid. + NotFoundResp: + description: Not found + BadRequestResp: + description: Invalid request. + RequestTooLarge: + description: Request Entity Too Large. + schemas: + ServerVersion: + type: object + properties: + version: + type: string + example: 2023.1.1 + major: + type: integer + example: 2023 + minor: + type: integer + example: 1 + fix: + nullable: true + type: integer + example: 1 + info_url: + nullable: true + type: string + example: "https://github.com/MerginMaps/mergin/releases/tag/2023.1" diff --git a/server/mergin/app.py b/server/mergin/app.py index 654d5c04..8910c6d1 100644 --- a/server/mergin/app.py +++ b/server/mergin/app.py @@ -177,6 +177,12 @@ def create_app(public_keys: List[str] = None) -> Flask: options={"swagger_ui": False, "serve_spec": False}, validate_responses=True, ) + app.add_api( + "api.yaml", + arguments={"title": "Mergin"}, + options={"swagger_ui": False, "serve_spec": False}, + validate_responses=True, + ) app.app.config.from_object(SyncConfig) app.app.connexion_app = app diff --git a/server/mergin/config.py b/server/mergin/config.py index f54c39cc..8855b5a8 100644 --- a/server/mergin/config.py +++ b/server/mergin/config.py @@ -7,6 +7,8 @@ from .version import get_version +config_dir = os.path.abspath(os.path.dirname(__file__)) + class Configuration(object): # flask/connexion variables @@ -111,3 +113,12 @@ class Configuration(object): "DIAGNOSTIC_LOGS_URL", default="", ) + DIAGNOSTIC_LOGS_DIR = config( + "DIAGNOSTIC_LOGS_DIR", + default=os.path.join( + config_dir, os.pardir, os.pardir, os.pardir, "diagnostic_logs" + ), + ) + DIAGNOSTIC_LOGS_MAX_SIZE = config( + "DIAGNOSTIC_LOGS_MAX_SIZE", default=1024 * 1024, cast=int + ) diff --git a/server/mergin/controller.py b/server/mergin/controller.py new file mode 100644 index 00000000..93824e68 --- /dev/null +++ b/server/mergin/controller.py @@ -0,0 +1,61 @@ +import json +import logging +import os +from flask import abort, current_app, request +from flask_login import current_user +from magic import from_buffer +import time + +import requests + +from .utils import save_diagnostic_log_file +from .app import parse_version_string, db + + +def get_latest_version(): + """Parse information about available server updates from 3rd party service""" + try: + req = requests.get(current_app.config["STATISTICS_URL"] + "/latest-versions") + except requests.exceptions.RequestException: + abort(400, "Updates information not available") + + if not req.ok: + abort(400, "Updates information not available") + + data = req.json().get(current_app.config["SERVER_TYPE"].lower(), None) + if not data: + abort(400, "Updates information not available") + + parsed_version = parse_version_string(data.get("version", "")) + if not parsed_version: + abort(400, "Updates information not available") + + data = {**data, **parsed_version} + return data, 200 + + +def save_diagnostic_log(): + """Save diagnostic logs""" + # if server is using external storage, we don't want to save logs + if current_app.config.get("DIAGNOSTIC_LOGS_URL"): + abort(404) + + # check if plain text body is not larger than 1MB + max_size = current_app.config.get("DIAGNOSTIC_LOGS_MAX_SIZE") + if request.content_length > max_size: + abort(413) + # get body from request + body = request.get_data() + if not body: + abort(400) + if len(body) > max_size: + abort(413) + mime_type = from_buffer(body, mime=True) + if mime_type != "text/plain": + abort(400) + + app = request.args.get("app") + username = current_user.username if current_user.is_authenticated else "anonymous" + save_diagnostic_log_file(app, username, body) + + return "Log saved successfully", 200 diff --git a/server/mergin/stats/api.yaml b/server/mergin/stats/api.yaml index e1a5d492..757351bd 100644 --- a/server/mergin/stats/api.yaml +++ b/server/mergin/stats/api.yaml @@ -6,22 +6,6 @@ info: servers: - url: / paths: - /v1/latest-version: - get: - summary: Fetch latest available server version - operationId: get_latest_version - responses: - "200": - description: Latest version info - content: - application/json: - schema: - $ref: "#/components/schemas/ServerVersion" - "400": - $ref: "#/components/responses/BadRequestResp" - "404": - $ref: "#/components/responses/NotFoundResp" - x-openapi-router-controller: mergin.stats.controller /app/admin/report: get: summary: Download statistics for server @@ -53,38 +37,6 @@ paths: $ref: "#/components/responses/BadRequestResp" "404": $ref: "#/components/responses/NotFoundResp" - /v2/diagnostic-logs: - post: - summary: Save diagnostic logs - operationId: save_diagnostic_log - x-openapi-router-controller: mergin.stats.controller - parameters: - - name: app - in: query - description: Application name (e.g., "input-android-0.9.0") - required: true - schema: - type: string - - name: username - in: query - description: Username - required: true - schema: - type: string - requestBody: - required: true - content: - text/plain: - schema: - type: string - description: Log content in plain text - responses: - "200": - description: Log saved successfully - "400": - $ref: "#/components/responses/BadRequestResp" - "413": - $ref: "#/components/responses/RequestTooLarge" components: responses: UnauthorizedError: @@ -93,26 +45,3 @@ components: description: Project not found. BadRequestResp: description: Invalid request. - RequestTooLarge: - description: Request Entity Too Large. - schemas: - ServerVersion: - type: object - properties: - version: - type: string - example: 2023.1.1 - major: - type: integer - example: 2023 - minor: - type: integer - example: 1 - fix: - nullable: true - type: integer - example: 1 - info_url: - nullable: true - type: string - example: "https://github.com/MerginMaps/mergin/releases/tag/2023.1" diff --git a/server/mergin/stats/config.py b/server/mergin/stats/config.py index e251e851..cfd5b4d9 100644 --- a/server/mergin/stats/config.py +++ b/server/mergin/stats/config.py @@ -6,9 +6,6 @@ from decouple import config -config_dir = os.path.abspath(os.path.dirname(__file__)) - - class Configuration(object): # send statistics about usage COLLECT_STATISTICS = config("COLLECT_STATISTICS", default=True, cast=bool) @@ -20,9 +17,3 @@ class Configuration(object): STATISTICS_URL = config( "STATISTICS_URL", default="https://api.merginmaps.com/monitoring/v1" ).rstrip("/") - DIAGNOSTIC_LOGS_DIR = config( - "DIAGNOSTIC_LOGS_DIR", - default=os.path.join( - config_dir, os.pardir, os.pardir, os.pardir, "diagnostic_logs" - ), - ) diff --git a/server/mergin/stats/controller.py b/server/mergin/stats/controller.py index 57f9eb49..d58a4417 100644 --- a/server/mergin/stats/controller.py +++ b/server/mergin/stats/controller.py @@ -3,17 +3,15 @@ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial import requests -from flask import abort, current_app, make_response, request +from flask import abort, current_app, make_response from datetime import datetime, time from csv import DictWriter -from pathvalidate import sanitize_filename from mergin.auth.app import auth_required from mergin.stats.models import MerginStatistics, ServerCallhomeData -from mergin.stats.utils import save_diagnostic_log_file from .config import Configuration -from ..app import parse_version_string, db +from ..app import db class CsvTextBuilder(object): @@ -28,28 +26,6 @@ def write(self, row): self.data.append(row) -def get_latest_version(): - """Parse information about available server updates from 3rd party service""" - try: - req = requests.get(Configuration.STATISTICS_URL + "/latest-versions") - except requests.exceptions.RequestException: - abort(400, "Updates information not available") - - if not req.ok: - abort(400, "Updates information not available") - - data = req.json().get(current_app.config["SERVER_TYPE"].lower(), None) - if not data: - abort(400, "Updates information not available") - - parsed_version = parse_version_string(data.get("version", "")) - if not parsed_version: - abort(400, "Updates information not available") - - data = {**data, **parsed_version} - return data, 200 - - @auth_required(permissions=["admin"]) def download_report(date_from: str, date_to: str): """Download statistics from server instance""" @@ -89,27 +65,3 @@ def download_report(date_from: str, date_to: str): response.headers["Content-Disposition"] = f"attachment; filename=usage-report.csv" response.mimetype = "text/csv" return response - - -def save_diagnostic_log(app: str, username: str): - """Save diagnostic logs""" - # if server is using external storage, we don't want to save logs - if current_app.config.get("DIAGNOSTIC_LOGS_URL"): - abort(404) - - # check if plain text body is not larger than 1MB - max_size = 1024 * 1024 - if request.content_length > max_size: - abort(413) - # get body from request - body = request.get_data() - if not body: - abort(400) - if len(body) > max_size: - abort(413) - - # save diagnostic log file - folder = current_app.config.get("DIAGNOSTIC_LOGS_DIR") - save_diagnostic_log_file(app, username, body, folder) - - return "Log saved successfully", 200 diff --git a/server/mergin/stats/utils.py b/server/mergin/stats/utils.py deleted file mode 100644 index ae0e1155..00000000 --- a/server/mergin/stats/utils.py +++ /dev/null @@ -1,21 +0,0 @@ -from datetime import datetime, timezone -import os - -from pathvalidate import sanitize_filename - - -def save_diagnostic_log_file( - app: str, username: str, body: bytes, to_folder: str -) -> str: - """Save diagnostic log file to DIAGNOSTIC_LOGS_DIR""" - - content = body.decode("utf-8") - datetime_iso_str = datetime.now(tz=timezone.utc).isoformat() - file_name = sanitize_filename( - username + "_" + app + "_" + datetime_iso_str + ".log" - ) - os.makedirs(to_folder, exist_ok=True) - with open(os.path.join(to_folder, file_name), "w") as f: - f.write(content) - - return file_name diff --git a/server/mergin/tests/test_controller.py b/server/mergin/tests/test_controller.py new file mode 100644 index 00000000..9a266832 --- /dev/null +++ b/server/mergin/tests/test_controller.py @@ -0,0 +1,131 @@ +import os +from flask import url_for, current_app +import json +from unittest.mock import MagicMock, patch + +import requests + +from ..auth.models import User +from ..app import db +from .utils import Response + + +def test_healthcheck(client): + # anonymous user + client.get(url_for("/.mergin_auth_controller_logout")) + client.application.config["WTF_CSRF_ENABLED"] = True + maint_file = current_app.config["MAINTENANCE_FILE"] + resp = client.post("/alive") + assert resp.status_code == 200 + resp_data = json.loads(resp.data) + print(resp.headers) + print(resp_data) + assert "processing_time_ms" in resp_data + print(type(resp_data)) + assert not resp_data["maintenance"] + + # create maintenance mode + with open(maint_file, "w+"): + resp = client.post("/alive") + assert resp.status_code == 200 + resp_data = json.loads(resp.data) + assert resp_data["maintenance"] + os.remove(maint_file) + + # tests with invalid method + resp = client.get("/alive") + assert resp.status_code == 405 + + # mock some db issue + _connect = db.engine.connect + db.engine.connect = MagicMock(side_effect=Exception("Some db issue")) + resp = client.post("/alive") + assert resp.status_code == 500 + # undo mock + db.engine.connect = _connect + + +def test_server_updates(client): + """Test proxy endpoint to fetch server updates information""" + assert client.application.config["SERVER_TYPE"] == "ce" + url = "/v1/latest-version" + + with patch("requests.get") as mock: + api_data = { + "ee": {"version": "2023.1.2", "info_url": "https://release-info.com"}, + "ce": {"version": "2023.1.2", "info_url": "https://release-info.com"}, + } + mock.return_value = Response(True, api_data) + resp = client.get(url) + assert resp.status_code == 200 + assert resp.json["version"] == api_data["ce"]["version"] + assert resp.json["major"] == 2023 + assert resp.json["minor"] == 1 + assert resp.json["fix"] == 2 + + # remove fix version + api_data["ce"]["version"] = "2023.2" + resp = client.get(url) + assert resp.status_code == 200 + assert resp.json["major"] == 2023 + assert resp.json["minor"] == 2 + assert resp.json["fix"] is None + + # invalid response + del api_data["ce"]["version"] + resp = client.get(url) + assert resp.status_code == 400 + + # 3rd party api failure + mock.side_effect = requests.exceptions.RequestException("Some failure") + resp = client.get(url) + assert resp.status_code == 400 + + +def test_save_diagnostic_log(client, app): + """Test save diagnostic log endpoint""" + user = User.query.filter(User.username == "mergin").first() + url = url_for("mergin_controller_save_diagnostic_log") + resp = client.post(url) + assert resp.status_code == 400 + + # bad request + resp = client.post(url, data="test") + assert resp.status_code == 400 + + url = url_for("mergin_controller_save_diagnostic_log", app="test_app") + + # too large request + max_size = app.config["DIAGNOSTIC_LOGS_MAX_SIZE"] + resp = client.post(url, data="x" * (max_size + 1)) + assert resp.status_code == 413 + + # invalid mime type + resp = client.post(url, data="#!/usr/bin/python\n") + assert resp.status_code == 400 + + # valid request + resp = client.post(url, data="test") + assert resp.status_code == 200 + + # check if file was created + log_dir = app.config["DIAGNOSTIC_LOGS_DIR"] + assert os.path.exists(log_dir) + files = os.listdir(log_dir) + assert len(files) == 1 + assert files[0].startswith(f"{user.username}_test_app_") + assert files[0].endswith(".log") + with open(os.path.join(log_dir, files[0]), "r") as f: + content = f.read() + assert content == "test" + os.remove(os.path.join(log_dir, files[0])) + + # anonymous user + client.get(url_for("/.mergin_auth_controller_logout")) + # valid request + resp = client.post(url, data="test") + assert resp.status_code == 200 + files = os.listdir(log_dir) + assert len(files) == 1 + assert files[0].startswith(f"anonymous_test_app_") + assert files[0].endswith(".log") diff --git a/server/mergin/tests/test_statistics.py b/server/mergin/tests/test_statistics.py index dc73ef96..189b04b0 100644 --- a/server/mergin/tests/test_statistics.py +++ b/server/mergin/tests/test_statistics.py @@ -5,14 +5,12 @@ from dataclasses import asdict from datetime import timezone, datetime import json -import os from unittest.mock import patch -from flask import url_for import requests from sqlalchemy.sql.operators import is_ from mergin.auth.models import User -from mergin.sync.models import Project, ProjectRole +from mergin.sync.models import ProjectRole from ..app import db from ..stats.tasks import get_callhome_data, save_statistics, send_statistics @@ -123,43 +121,6 @@ def test_send_statistics(app, caplog): assert info.last_reported -def test_server_updates(client): - """Test proxy endpoint to fetch server updates information""" - assert client.application.config["SERVER_TYPE"] == "ce" - url = "/v1/latest-version" - - with patch("requests.get") as mock: - api_data = { - "ee": {"version": "2023.1.2", "info_url": "https://release-info.com"}, - "ce": {"version": "2023.1.2", "info_url": "https://release-info.com"}, - } - mock.return_value = Response(True, api_data) - resp = client.get(url) - assert resp.status_code == 200 - assert resp.json["version"] == api_data["ce"]["version"] - assert resp.json["major"] == 2023 - assert resp.json["minor"] == 1 - assert resp.json["fix"] == 2 - - # remove fix version - api_data["ce"]["version"] = "2023.2" - resp = client.get(url) - assert resp.status_code == 200 - assert resp.json["major"] == 2023 - assert resp.json["minor"] == 2 - assert resp.json["fix"] is None - - # invalid response - del api_data["ce"]["version"] - resp = client.get(url) - assert resp.status_code == 400 - - # 3rd party api failure - mock.side_effect = requests.exceptions.RequestException("Some failure") - resp = client.get(url) - assert resp.status_code == 400 - - def test_save_statistics(app, client): """Test save statistics celery job""" info = MerginInfo.query.first() @@ -221,39 +182,3 @@ def test_download_report(app, client): empty_file = f"{','.join(keys)}\r\n" assert resp.data.decode("UTF-8") == empty_file assert len(lines) == 1 - - -def test_save_diagnostic_log(client, app): - """Test save diagnostic log endpoint""" - url = url_for("/.mergin_stats_controller_save_diagnostic_log") - resp = client.post(url) - assert resp.status_code == 400 - - # bad request - resp = client.post(url, data="test") - assert resp.status_code == 400 - - url = url_for( - "/.mergin_stats_controller_save_diagnostic_log", - app="test_app", - username="test_user", - ) - - # too large request - resp = client.post(url, data="x" * (1024 * 1024 + 1)) - assert resp.status_code == 413 - - # valid request - resp = client.post(url, data="test") - assert resp.status_code == 200 - - # check if file was created - log_dir = app.config["DIAGNOSTIC_LOGS_DIR"] - assert os.path.exists(log_dir) - files = os.listdir(log_dir) - assert len(files) == 1 - assert files[0].startswith("test_user_test_app_") - assert files[0].endswith(".log") - with open(os.path.join(log_dir, files[0]), "r") as f: - content = f.read() - assert content == "test" diff --git a/server/mergin/tests/test_statistics_utils.py b/server/mergin/tests/test_statistics_utils.py deleted file mode 100644 index fd83ebc2..00000000 --- a/server/mergin/tests/test_statistics_utils.py +++ /dev/null @@ -1,37 +0,0 @@ -from datetime import datetime, timezone -import os -from unittest.mock import patch - -from pathvalidate import sanitize_filename - -from ..stats.utils import save_diagnostic_log_file - - -def test_save_diagnostic_log_file(client, app): - """Test save diagnostic log file""" - # Mock datetime value - test_date = "2025-05-09T12:00:00+00:00" - app_name = "t" * 256 - username = "test-user" - body = b"Test log content" - to_folder = app.config["DIAGNOSTIC_LOGS_DIR"] - - saved_file_name = save_diagnostic_log_file(app_name, username, body, to_folder) - saved_file_path = os.path.join(to_folder, saved_file_name) - assert os.path.exists(saved_file_path) - assert len(saved_file_name) == 255 - - with patch("mergin.stats.utils.datetime") as mock_datetime: - mock_datetime.now.return_value = datetime.fromisoformat(test_date) - app_name = "test_<>app" - saved_file_name = save_diagnostic_log_file(app_name, username, body, to_folder) - # Check if the file was created - assert saved_file_name == sanitize_filename( - username + "_" + app_name + "_" + test_date + ".log" - ) - saved_file_path = os.path.join(to_folder, saved_file_name) - assert os.path.exists(saved_file_path) - # Check the content of the file - with open(saved_file_path, "r") as f: - content = f.read() - assert content == body.decode("utf-8") diff --git a/server/mergin/tests/test_utils.py b/server/mergin/tests/test_utils.py index f3249daa..514928ea 100644 --- a/server/mergin/tests/test_utils.py +++ b/server/mergin/tests/test_utils.py @@ -3,14 +3,18 @@ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial import base64 +from datetime import datetime import json import os import pytest from flask import url_for, current_app from sqlalchemy import desc -from unittest.mock import MagicMock +import os +from unittest.mock import patch +from pathvalidate import sanitize_filename + +from ..utils import save_diagnostic_log_file -from ..app import db from ..sync.utils import ( parse_gpkgb_header_size, gpkg_wkb_to_wkt, @@ -109,36 +113,6 @@ def test_parse_gpkg(): assert not wkt -def test_healthcheck(client): - client.application.config["WTF_CSRF_ENABLED"] = True - maint_file = current_app.config["MAINTENANCE_FILE"] - resp = client.post("/alive") - assert resp.status_code == 200 - resp_data = json.loads(resp.data) - assert "processing_time_ms" in resp_data - assert not resp_data["maintenance"] - - # create maintenance mode - with open(maint_file, "w+"): - resp = client.post("/alive") - assert resp.status_code == 200 - resp_data = json.loads(resp.data) - assert resp_data["maintenance"] - os.remove(maint_file) - - # tests with invalid method - resp = client.get("/alive") - assert resp.status_code == 405 - - # mock some db issue - _connect = db.engine.connect - db.engine.connect = MagicMock(side_effect=Exception("Some db issue")) - resp = client.post("/alive") - assert resp.status_code == 500 - # undo mock - db.engine.connect = _connect - - def test_is_name_allowed(): test_cases = [ ("project", True), @@ -270,3 +244,33 @@ def test_get_x_accell_uri(client): url_parts = () assert get_x_accel_uri(*url_parts) == "/download" + + +def test_save_diagnostic_log_file(client, app): + """Test save diagnostic log file""" + # Mock datetime value + test_date = "2025-05-09T12:00:00+00:00" + app_name = "t" * 256 + username = "test-user" + body = b"Test log content" + to_folder = app.config["DIAGNOSTIC_LOGS_DIR"] + + saved_file_name = save_diagnostic_log_file(app_name, username, body) + saved_file_path = os.path.join(to_folder, saved_file_name) + assert os.path.exists(saved_file_path) + assert len(saved_file_name) == 255 + + with patch("mergin.utils.datetime") as mock_datetime: + mock_datetime.now.return_value = datetime.fromisoformat(test_date) + app_name = "test_<>app" + saved_file_name = save_diagnostic_log_file(app_name, username, body) + # Check if the file was created + assert saved_file_name == sanitize_filename( + username + "_" + app_name + "_" + test_date + ".log" + ) + saved_file_path = os.path.join(to_folder, saved_file_name) + assert os.path.exists(saved_file_path) + # Check the content of the file + with open(saved_file_path, "r") as f: + content = f.read() + assert content == body.decode("utf-8") diff --git a/server/mergin/utils.py b/server/mergin/utils.py index c570acf5..9acc6124 100644 --- a/server/mergin/utils.py +++ b/server/mergin/utils.py @@ -3,9 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial import math from collections import namedtuple -from datetime import timedelta +from datetime import datetime, timedelta, timezone from enum import Enum +import os +from flask import current_app from flask_sqlalchemy import Model +from pathvalidate import sanitize_filename from sqlalchemy import Column, JSON from sqlalchemy.sql.elements import UnaryExpression from typing import Optional @@ -116,3 +119,19 @@ def format_time_delta(delta: timedelta) -> str: else: difference = "N/A" return difference + + +def save_diagnostic_log_file(app: str, username: str, body: bytes) -> str: + """Save diagnostic log file to DIAGNOSTIC_LOGS_DIR""" + + content = body.decode("utf-8") + datetime_iso_str = datetime.now(tz=timezone.utc).isoformat() + file_name = sanitize_filename( + username + "_" + app + "_" + datetime_iso_str + ".log" + ) + to_folder = current_app.config.get("DIAGNOSTIC_LOGS_DIR") + os.makedirs(to_folder, exist_ok=True) + with open(os.path.join(to_folder, file_name), "w") as f: + f.write(content) + + return file_name From 5c295e7abbb0e5d001e11955dacee71ac879cd63 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Tue, 13 May 2025 17:07:43 +0200 Subject: [PATCH 13/17] Added diagnostic logs to deployment --- deployment/community/README.md | 2 ++ deployment/community/docker-compose.yml | 1 + deployment/enterprise/.env.template | 3 +++ deployment/enterprise/docker-compose.yml | 1 + 4 files changed, 7 insertions(+) diff --git a/deployment/community/README.md b/deployment/community/README.md index e99b90c3..9f6ab420 100644 --- a/deployment/community/README.md +++ b/deployment/community/README.md @@ -13,4 +13,6 @@ cp .env.template .prod.env Next step is to create data directory for mergin maps `projects` with proper permissions. Should you prefer a different location, please do search and replace it in config files (`.prod.env`, `docker-compose.yml`). Make sure your volume is large enough since mergin maps keeps all projects files, their history and also needs some space for temporary processing. +If you want to persist diagnostic logs, create data directory `diagnostic_logs` with proper permissions. + For more details about deployment please check [docs](https://merginmaps.com/docs/server/install/#deployment). diff --git a/deployment/community/docker-compose.yml b/deployment/community/docker-compose.yml index 9ee27549..8b5d5156 100644 --- a/deployment/community/docker-compose.yml +++ b/deployment/community/docker-compose.yml @@ -29,6 +29,7 @@ services: user: 901:999 volumes: - ./projects:/data + - ./diagnostic_logs:/diagnostic_logs - ../common/entrypoint.sh:/app/entrypoint.sh env_file: - .prod.env diff --git a/deployment/enterprise/.env.template b/deployment/enterprise/.env.template index f6bda021..762bdd14 100644 --- a/deployment/enterprise/.env.template +++ b/deployment/enterprise/.env.template @@ -215,3 +215,6 @@ VECTOR_TILES_STYLE_URL=https://tiles-ee.merginmaps.com//styles/default.json #QGIS_EXTRACTOR_TIMEOUT=60 #OVERVIEW_MAX_FILE_SIZE=1048576 # 1MB + +# Diagnostic logs from Mobile and QGIS Plugin +DIAGNOSTIC_LOGS_DIR=/diagnostic_logs diff --git a/deployment/enterprise/docker-compose.yml b/deployment/enterprise/docker-compose.yml index cb5084c8..3bf085e4 100644 --- a/deployment/enterprise/docker-compose.yml +++ b/deployment/enterprise/docker-compose.yml @@ -12,6 +12,7 @@ services: command: ["gunicorn -w 4 --config config.py application:application"] volumes: - ./data:/data # map data dir to host + - ./diagnostic_logs:/diagnostic_logs # diagnostic logs dir - ../common/entrypoint.sh:/app/entrypoint.sh env_file: - .prod.env From 5cc2883dd3ed5562d9eac314b017457345679197 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 22 May 2025 10:46:08 +0200 Subject: [PATCH 14/17] check_password method can handle epmty password field --- server/Pipfile.lock | 762 +++++++++++++++---------------- server/mergin/auth/models.py | 4 +- server/mergin/tests/test_auth.py | 15 + 3 files changed, 399 insertions(+), 382 deletions(-) diff --git a/server/Pipfile.lock b/server/Pipfile.lock index 438912f8..1e02fadf 100644 --- a/server/Pipfile.lock +++ b/server/Pipfile.lock @@ -117,11 +117,11 @@ }, "certifi": { "hashes": [ - "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", - "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" + "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", + "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3" ], "markers": "python_version >= '3.6'", - "version": "==2025.1.31" + "version": "==2025.4.26" }, "chardet": { "hashes": [ @@ -133,109 +133,109 @@ }, "charset-normalizer": { "hashes": [ - "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", - "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", - "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", - "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", - "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", - "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", - "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", - "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", - "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", - "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", - "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", - "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", - "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", - "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", - "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", - "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", - "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", - "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", - "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", - "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", - "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", - "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", - "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", - "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", - "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", - "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", - "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", - "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", - "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", - "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", - "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", - "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", - "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", - "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", - "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", - "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", - "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", - "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", - "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", - "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", - "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", - "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", - "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", - "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", - "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", - "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", - "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", - "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", - "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", - "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", - "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", - "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", - "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", - "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", - "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", - "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", - "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", - "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", - "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", - "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", - "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", - "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", - "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", - "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", - "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", - "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", - "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", - "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", - "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", - "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", - "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", - "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", - "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", - "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", - "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", - "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", - "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", - "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", - "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", - "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", - "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", - "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", - "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", - "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", - "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", - "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", - "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", - "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", - "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", - "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", - "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", - "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", + "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", + "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", + "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", + "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", + "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", + "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", + "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", + "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", + "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", + "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", + "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", + "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", + "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", + "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", + "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", + "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", + "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", + "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", + "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", + "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", + "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", + "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", + "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", + "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", + "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", + "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", + "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", + "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", + "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", + "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", + "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", + "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", + "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", + "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", + "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", + "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", + "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", + "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", + "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", + "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", + "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", + "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", + "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", + "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", + "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", + "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", + "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", + "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", + "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", + "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", + "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", + "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", + "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", + "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", + "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", + "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", + "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", + "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", + "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", + "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", + "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", + "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", + "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", + "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", + "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", + "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", + "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", + "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", + "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", + "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", + "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", + "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", + "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", + "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", + "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", + "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", + "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", + "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", + "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", + "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", + "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", + "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", + "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", + "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", + "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", + "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", + "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", + "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", + "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", + "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", + "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" ], "markers": "python_version >= '3.7'", - "version": "==3.4.1" + "version": "==3.4.2" }, "click": { "hashes": [ - "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", - "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", + "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.8" + "markers": "python_version >= '3.10'", + "version": "==8.2.0" }, "click-didyoumean": { "hashes": [ @@ -373,108 +373,108 @@ }, "gevent": { "hashes": [ - "sha256:036cfaf14f90bd8aa57fce4a875693d3712c446371f3812eb113860f7393f670", - "sha256:1bdd383c77a47e041844a17cb43c15c2e1e0b2749fc85d4a2cff14307044b78a", - "sha256:1cb0ec13cace36c38ec4648a78ad93192f45981d1e79f029d456f8305a2ca2bb", - "sha256:29d05fa5e566d04ca509d2c25b15f5e9b123cac4f2f96bb85e110740860e107f", - "sha256:2f31e40ecf11e19150c11bf6b2977050360020ae6086d4c21f94455e3d0222ee", - "sha256:347c60704fa8b6b3110a376facc69ae405ab4d8a71cb1012f2de24e8059a080b", - "sha256:43800850e138913557c96e4a45f86e017b52e5bc63d14395ffd73ebff26e0031", - "sha256:450cdc9a99c665049eaea2257d3b9c7e1cf9a5a699901b1dfc85f1061e6d6cf2", - "sha256:4e0ad8db3711e56c40d37a1b80c61a27f7523de33c5338498ceb453f837a3368", - "sha256:5a8e5660925285b7befb1ecbecc8d44a0e91cdec2b1eebc24006cec564ab8ec5", - "sha256:5f8c584b946f1b476f23bf03b64c4e21f8e6273faa8cf8c91e199491dc1a2b2e", - "sha256:61cbe95833751648fae1aa62d50d386fac71658f57ffa76b3140056f5b305086", - "sha256:63b906c23cc27343d74e605f3ce6ee60947b912737e50b5e1b1a56e6adb54ad6", - "sha256:6cafc4b2e2f55f84acd52c909a2cfda6375e08c1c294536ced473947869f84e8", - "sha256:702ca9a4b75f549c235da6ab9d43fc5261ee7aeaccc6de40f92c43917593ec7f", - "sha256:7052c0daeb260730291a58c6e5cf3789735f8c937557e6a0231d4f46027771b1", - "sha256:7135b10f82864e40d25e877419fa1f90f770dc568917513af0e5ba63910e0067", - "sha256:7c82179a3b1783d0ecf2a11e2040f4f12d7e1925c31e888fe69217cc4e2e21ba", - "sha256:82449386e57237756b10f284cd51172edf4b3866ace143f60d196459aa411c32", - "sha256:832dd696e7bd50c7f141c3af0f9470bc67128152df040580d39c22084d9bb3a5", - "sha256:88cada934c24de811df209d4d5d0cbb78f88a9c3f06ba491ff08c19f6625f9fa", - "sha256:8e5ad1016f7699b6b22186509c64d1b1fcf4df179420062adfebf22fc48bad32", - "sha256:9c59dd3aab086092510860a5c9d237429181cca96e684ba052d27e6a45c78345", - "sha256:b3154899cc60ae1814fb232b6055f8286d7825250d9e3fb06c4f52be126e46cd", - "sha256:b43cbe919a22c1b640152ba828dbb72b3b0b600714a481a0240884afe680ae28", - "sha256:b59c8e7c19b12d04be133b0cea311239069e816e5a7aefc871f3984ba3bcbfb0", - "sha256:b661ead80588352ba94fd55f64432e1eb9a55a259c3a68872377e3c90b087939", - "sha256:cd8762bae8633da18884074a3b7ea1384c1c64131ae6e36ecc927675df258e1f", - "sha256:cfcbd6cc35449d9346234227d90253bfc2c015027e6b076b9c205633ca7e7ad9", - "sha256:e810fc35e4bb8e90fb6771b7f4694e0baf6ce28d1e4740afc4133834eca8b219", - "sha256:e940d31f737925d084bcf2b593e0ba04ade04c9cd21a6230b79081af0cd8fb23", - "sha256:e98cc6262b20471285f8d81e14155d0d4c2a186d31d49b48b71ab5ba6f24d687", - "sha256:ec53625dc995086f27bf49be425f3111a8105e04385830266c021c541bd23998", - "sha256:ee11cd11ee7fbe5215b6d37a9eb32a49fd210540d757648dd991128995e73575", - "sha256:f3de1700459dc16cabaca4609ad83f7cd1371db445d30da1d4aee2e7674ea123", - "sha256:f5f51f6fb68e8c47a02b9c16634bbd9b43b2ff6a66c52c71e6a406dd6b07c80c", - "sha256:fa9bb5133bd50e46d3a8445eb8da829150212d6addfac6c89a2298281e5b884d", - "sha256:fb2701b751f36c39d1dd4dbcc4702089f198673ebe3bcafe3497af6026dbcdfe", - "sha256:ff03881d6b30efb33c110188d6d5c1c10fb09ab299e5ef78db57ae5fc057ea9a" - ], - "version": "==25.4.1" + "sha256:03587078c402aee27231ecaabd81aec1e8b3de2629830fbd4486e2d09e638ddc", + "sha256:0cc1d6093f482547ac522ab1a985429d8c12494518eeca354c956f0ff6de7a94", + "sha256:11bc2374ce3f1db3a243522c4d30b9e86e2dc0f2905f083fff288afa8ef8031f", + "sha256:12b596c027cf546a235231d421473483fdf7fa586d38162d36b07c8efa9081ba", + "sha256:2270a8607661e609c44e4f72811b6380dcfede558041e4ee3134e66753865038", + "sha256:22f33261b32e28433af7a96388ce33b77e903a648fc868b993304af2c1bca05b", + "sha256:43469ed40ea6cfb1c88e8d85a57aa5f52dd6b3b94a2e499752ab7e60a90c7dba", + "sha256:44acca4196d4a174c2b4817642564526898f42f72992dc1818b834b2bbf17582", + "sha256:498f548330c4724e3b0cee0d75551165fc9e4309ae3ddcba3d644aaa866ca9c3", + "sha256:5940174c7d1ffc7bb4b0ea9f2908f4f361eb03ada9e145d3590b8df1e61c379b", + "sha256:63aecf1e43b8d01086ea574ed05f7272ed40c48dd41fa3d061e3c5ca900abcdd", + "sha256:677e5d1c7d0a0b4240644321f10b8e3b36fd4ca5fc1b45d0e4989e6884375537", + "sha256:6c1d1a66a28372d505e0d8f6f1fdb62f7d5b3423e49431f41b99bd9133f006b7", + "sha256:7442b3ffac08f6239d6463ee2943fd9a619b64b2db11cec292acf8caccb70536", + "sha256:75d2fdd24f3948c085d341281648014760f5cb23de9b29f710083e6911b2e605", + "sha256:76c440972ff57eb64e089f85210ccc0fa247ab71cdedff5414c6b86392f7f791", + "sha256:7ffba461458ed28a85a01285ea0e0dc14f883204d17ce5ed82fa839a9d620028", + "sha256:8b90913360b1af058b279160679d804d4917a8661f128b2f7625f8665c39450f", + "sha256:8e740bc08ba4c34951f4bb6351dbe04209416e12d620691fb57e115b218a7818", + "sha256:9100693f2bd8237ce7ce99a2b62da128196d8abcda331049e67ad6afb8cff23a", + "sha256:91408dd197c13ca0f1e0d5cdcc9870c674963bb87a7e370b2884d1426d73834f", + "sha256:95790dd8aeb4ca8df9ac215ec353a29108647797e54daa652a4634ca316f70d4", + "sha256:a7c70ab6d33dfeb43bfe982c636609d8f90506dacaaa1f409a3c43c66d578fb1", + "sha256:b0a656eccd9cb115d01c9bbe55bfe84cf20c8422c495503f41aef747b193c33d", + "sha256:b7ae7ad4ff9c4492d4b633702e35153509b07dc6ffd20f1577076d7647c9caba", + "sha256:b91e862ab0ddecf37ee6e3bf33965ef4c3e38ba9cdc106eef552293caed512f9", + "sha256:c535d96ded6e26b37fadda9242a49fea6308754da5945173940614b7520c07b4", + "sha256:c62bf14557d2cb54f5e3c1ba0a3b3f4b69bf0441081c32d63b205763b495b251", + "sha256:ccbc835939416a7df7834b79c655409a2a9d2deb9bf119b28dedf72a168f7895", + "sha256:cd59c0dbcae2808a1e26e07d3858b5a935635be195c8ea967a4bc32599381523", + "sha256:d68fdf9bff0068367126983d7d85765124c292b4bc3d4d19ed8138335d8426a7", + "sha256:d7999e4d4b3597b706a333f9a7bf2efbd8365cd244312405f33b4870fa3b411d", + "sha256:eb89ed32e2b766fcb1afc52847e33d8c369d2b40f23d4c96977fd092b5a0ea86", + "sha256:f12e570777027f807dc7dc3ea1945ea040befaf1c9485deb6f24d7110009fc12", + "sha256:f735f57bc19d0f8bbc784093cfb7953a9ad66612b05c3ff876ec7951a96d7edd", + "sha256:fdf9aec76a7285b00fb64ec942cd9ff88f8765874a5abf99c4e8c5374b3133e9", + "sha256:fe4a3e3fa3a16ed9b12b6ff0922208ef83287e066e696b82b96d33723d8207f2", + "sha256:feb5f2f44dcdad1a6b80e7ce24e7557ce25d01ff13b7a74ca276d113adf9d4af", + "sha256:ff92408011d78e4ffe297331ff30cded39a3e22845ba237516c646f6a485a241" + ], + "version": "==25.4.2" }, "greenlet": { "hashes": [ - "sha256:0010e928e1901d36625f21d008618273f9dda26b516dbdecf873937d39c9dff0", - "sha256:04e781447a4722e30b4861af728cb878d73a3df79509dc19ea498090cea5d204", - "sha256:0e14541f9024a280adb9645143d6a0a51fda6f7c5695fd96cb4d542bb563442f", - "sha256:144283ad88ed77f3ebd74710dd419b55dd15d18704b0ae05935766a93f5671c5", - "sha256:17fd241c0d50bacb7ce8ff77a30f94a2d0ca69434ba2e0187cf95a5414aeb7e1", - "sha256:18adc14ab154ca6e53eecc9dc50ff17aeb7ba70b7e14779b26e16d71efa90038", - "sha256:199453d64b02d0c9d139e36d29681efd0e407ed8e2c0bf89d88878d6a787c28f", - "sha256:1cf89e2d92bae0d7e2d6093ce0bed26feeaf59a5d588e3984e35fcd46fc41090", - "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7", - "sha256:1dcb1108449b55ff6bc0edac9616468f71db261a4571f27c47ccf3530a7f8b97", - "sha256:211a9721f540e454a02e62db7956263e9a28a6cf776d4b9a7213844e36426333", - "sha256:23f56a0103deb5570c8d6a0bb4ddf8a7a28931973ad7ed7a883460a67e599b32", - "sha256:2688b3bd3198cc4bad7a79648a95fee088c24a0f6abd05d3639e6c3040ded015", - "sha256:2919b126eeb63ca5fa971501cd20cd6cdb5522369a8e39548bbc73a3e10b8b41", - "sha256:29449a2b82ed7ce11f8668c31ef20d31e9d88cd8329eb933098fab5a8608a93a", - "sha256:2b986f1a6467710e7ffeeeac1777da0318c95bbfcc467acbd0bd35abc775f558", - "sha256:33ea7e7269d6f7275ce31f593d6dcfedd97539c01f63fbdc8d84e493e20b1b2c", - "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b", - "sha256:39801e633a978c3f829f21022501e7b0c3872683d7495c1850558d1a6fb95ed0", - "sha256:4174fa6fa214e8924cedf332b6f2395ba2b9879f250dacd3c361b2fca86f58af", - "sha256:430cba962c85e339767235a93450a6aaffed6f9c567e73874ea2075f5aae51e1", - "sha256:47aeadd1e8fbdef8fdceb8fb4edc0cbb398a57568d56fd68f2bc00d0d809e6b6", - "sha256:58ef3d637c54e2f079064ca936556c4af3989144e4154d80cfd4e2a59fc3769c", - "sha256:598da3bd464c2cc411b723e3d4afc27b13c219ac077ba897bac88443ae45f5ec", - "sha256:5be69cd50994b8465c3ad1467f9e63001f76e53a89440ad4440d1b6d52591280", - "sha256:5e57ff52315bfc0c5493917f328b8ba3ae0c0515d94524453c4d24e7638cbb53", - "sha256:6005f7a86de836a1dc4b8d824a2339cdd5a1ca7cb1af55ea92575401f9952f4c", - "sha256:6017a4d430fad5229e397ad464db504ae70cb7b903757c4688cee6c25d6ce8d8", - "sha256:60e77242e38e99ecaede853755bbd8165e0b20a2f1f3abcaa6f0dceb826a7411", - "sha256:6fad8a9ca98b37951a053d7d2d2553569b151cd8c4ede744806b94d50d7f8f73", - "sha256:7154b13ef87a8b62fc05419f12d75532d7783586ad016c57b5de8a1c6feeb517", - "sha256:78b721dfadc60e3639141c0e1f19d23953c5b4b98bfcaf04ce40f79e4f01751c", - "sha256:7b162de2fb61b4c7f4b5d749408bf3280cae65db9b5a6aaf7f922ac829faa67c", - "sha256:7b17a26abc6a1890bf77d5d6b71c0999705386b00060d15c10b8182679ff2790", - "sha256:7d08b88ee8d506ca1f5b2a58744e934d33c6a1686dd83b81e7999dfc704a912f", - "sha256:7f163d04f777e7bd229a50b937ecc1ae2a5b25296e6001445e5433e4f51f5191", - "sha256:7fee6f518868e8206c617f4084a83ad4d7a3750b541bf04e692dfa02e52e805d", - "sha256:82a68a25a08f51fc8b66b113d1d9863ee123cdb0e8f1439aed9fc795cd6f85cf", - "sha256:844acfd479ee380f3810415e682c9ee941725fb90b45e139bb7fd6f85c6c9a30", - "sha256:8a8940a8d301828acd8b9f3f85db23069a692ff2933358861b19936e29946b95", - "sha256:8b3538711e7c0efd5f7a8fc1096c4db9598d6ed99dc87286b31e4ce9f8a8da67", - "sha256:8fd2583024ff6cd5d4f842d446d001de4c4fe1264fdb5f28ddea28f6488866df", - "sha256:a0bc5776ac2831c022e029839bf1b9d3052332dcf5f431bb88c8503e27398e31", - "sha256:b2392cc41eeed4055978c6b52549ccd9effd263bb780ffd639c0e1e7e2055ab0", - "sha256:b7a7b7f2bad3ca72eb2fa14643f1c4ca11d115614047299d89bc24a3b11ddd09", - "sha256:b86a3ccc865ae601f446af042707b749eebc297928ea7bd0c5f60c56525850be", - "sha256:b99de16560097b9984409ded0032f101f9555e1ab029440fc6a8b5e76dbba7ac", - "sha256:cd37273dc7ca1d5da149b58c8b3ce0711181672ba1b09969663905a765affe21", - "sha256:ce531d7c424ef327a391de7a9777a6c93a38e1f89e18efa903a1c4ba11f85905", - "sha256:d3f32d7c70b1c26844fd0e4e56a1da852b493e4e1c30df7b07274a1e5a9b599e", - "sha256:d97bc1be4bad83b70d8b8627ada6724091af41139616696e59b7088f358583b9", - "sha256:e61d426969b68b2170a9f853cc36d5318030494576e9ec0bfe2dc2e2afa15a68", - "sha256:e8622b33d8694ec373ad55050c3d4e49818132b44852158442e1931bb02af336", - "sha256:e8ac9a2c20fbff3d0b853e9ef705cdedb70d9276af977d1ec1cde86a87a4c821", - "sha256:ee59db626760f1ca8da697a086454210d36a19f7abecc9922a2374c04b47735b" + "sha256:00cd814b8959b95a546e47e8d589610534cfb71f19802ea8a2ad99d95d702057", + "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207", + "sha256:02f5972ff02c9cf615357c17ab713737cccfd0eaf69b951084a9fd43f39833d3", + "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4", + "sha256:0a16fb934fcabfdfacf21d79e6fed81809d8cd97bc1be9d9c89f0e4567143d7b", + "sha256:1592a615b598643dbfd566bac8467f06c8c8ab6e56f069e573832ed1d5d528cc", + "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825", + "sha256:1e4747712c4365ef6765708f948acc9c10350719ca0545e362c24ab973017370", + "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708", + "sha256:1f72667cc341c95184f1c68f957cb2d4fc31eef81646e8e59358a10ce6689457", + "sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763", + "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf", + "sha256:3091bc45e6b0c73f225374fefa1536cd91b1e987377b12ef5b19129b07d93ebe", + "sha256:354f67445f5bed6604e493a06a9a49ad65675d3d03477d38a4db4a427e9aad0e", + "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d", + "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59", + "sha256:3aeca9848d08ce5eb653cf16e15bb25beeab36e53eb71cc32569f5f3afb2a3aa", + "sha256:44671c29da26539a5f142257eaba5110f71887c24d40df3ac87f1117df589e0e", + "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51", + "sha256:4bd139e4943547ce3a56ef4b8b1b9479f9e40bb47e72cc906f0f66b9d0d5cab3", + "sha256:4fefc7aa68b34b9224490dfda2e70ccf2131368493add64b4ef2d372955c207e", + "sha256:6629311595e3fe7304039c67f00d145cd1d38cf723bb5b99cc987b23c1433d61", + "sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5", + "sha256:71566302219b17ca354eb274dfd29b8da3c268e41b646f330e324e3967546a74", + "sha256:7409796591d879425997a518138889d8d17e63ada7c99edc0d7a1c22007d4907", + "sha256:752f0e79785e11180ebd2e726c8a88109ded3e2301d40abced2543aa5d164275", + "sha256:7791dcb496ec53d60c7f1c78eaa156c21f402dda38542a00afc3e20cae0f480f", + "sha256:782743700ab75716650b5238a4759f840bb2dcf7bff56917e9ffdf9f1f23ec59", + "sha256:7c9896249fbef2c615853b890ee854f22c671560226c9221cfd27c995db97e5c", + "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf", + "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b", + "sha256:8cb8553ee954536500d88a1a2f58fcb867e45125e600e80f586ade399b3f8819", + "sha256:9ae572c996ae4b5e122331e12bbb971ea49c08cc7c232d1bd43150800a2d6c65", + "sha256:9c7b15fb9b88d9ee07e076f5a683027bc3befd5bb5d25954bb633c385d8b737e", + "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", + "sha256:a31ead8411a027c2c4759113cf2bd473690517494f3d6e4bf67064589afcd3c5", + "sha256:a8fa80665b1a29faf76800173ff5325095f3e66a78e62999929809907aca5659", + "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485", + "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec", + "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8", + "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418", + "sha256:bf3fc9145141250907730886b031681dfcc0de1c158f3cc51c092223c0f381ce", + "sha256:c23ea227847c9dbe0b3910f5c0dd95658b607137614eb821e6cbaecd60d81cc6", + "sha256:c3cc1a3ed00ecfea8932477f729a9f616ad7347a5e55d50929efa50a86cb7be7", + "sha256:c49e9f7c6f625507ed83a7485366b46cbe325717c60837f7244fc99ba16ba9d6", + "sha256:d0cb7d47199001de7658c213419358aa8937df767936506db0db7ce1a71f4a2f", + "sha256:d8009ae46259e31bc73dc183e402f548e980c96f33a6ef58cc2e7865db012e13", + "sha256:da956d534a6d1b9841f95ad0f18ace637668f680b1339ca4dcfb2c1837880a0b", + "sha256:dcb9cebbf3f62cb1e5afacae90761ccce0effb3adaa32339a0670fe7805d8068", + "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325", + "sha256:df4d1509efd4977e6a844ac96d8be0b9e5aa5d5c77aa27ca9f4d3f92d3fcf330", + "sha256:eeb27bece45c0c2a5842ac4c5a1b5c2ceaefe5711078eed4e8043159fa05c834", + "sha256:efcdfb9df109e8a3b475c016f60438fcd4be68cd13a365d42b35914cdab4bb2b", + "sha256:fd9fb7c941280e2c837b603850efc93c999ae58aae2b40765ed682a6907ebbc5", + "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421" ], "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", - "version": "==3.2.0" + "version": "==3.2.2" }, "gunicorn": { "extras": [ @@ -548,11 +548,11 @@ }, "jsonschema-specifications": { "hashes": [ - "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", - "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" + "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", + "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608" ], "markers": "python_version >= '3.9'", - "version": "==2024.10.1" + "version": "==2025.4.1" }, "kombu": { "hashes": [ @@ -657,72 +657,72 @@ }, "numpy": { "hashes": [ - "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", - "sha256:0d54974f9cf14acf49c60f0f7f4084b6579d24d439453d5fc5805d46a165b542", - "sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f", - "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", - "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", - "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", - "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", - "sha256:218f061d2faa73621fa23d6359442b0fc658d5b9a70801373625d958259eaca3", - "sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146", - "sha256:2fa8fa7697ad1646b5c93de1719965844e004fcad23c91228aca1cf0800044a1", - "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", - "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", - "sha256:4ba5054787e89c59c593a4169830ab362ac2bee8a969249dc56e5d7d20ff8df9", - "sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592", - "sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00", - "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", - "sha256:7051ee569db5fbac144335e0f3b9c2337e0c8d5c9fee015f259a5bd70772b7e8", - "sha256:7716e4a9b7af82c06a2543c53ca476fa0b57e4d760481273e09da04b74ee6ee2", - "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", - "sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb", - "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", - "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", - "sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9", - "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", - "sha256:892c10d6a73e0f14935c31229e03325a7b3093fafd6ce0af704be7f894d95687", - "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", - "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", - "sha256:9eeea959168ea555e556b8188da5fa7831e21d91ce031e95ce23747b7609f8a4", - "sha256:a0258ad1f44f138b791327961caedffbf9612bfa504ab9597157806faa95194a", - "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", - "sha256:a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4", - "sha256:a84eda42bd12edc36eb5b53bbcc9b406820d3353f1994b6cfe453a33ff101775", - "sha256:ab2939cd5bec30a7430cbdb2287b63151b77cf9624de0532d629c9a1c59b1d5c", - "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", - "sha256:adf8c1d66f432ce577d0197dceaac2ac00c0759f573f28516246351c58a85020", - "sha256:b4adfbbc64014976d2f91084915ca4e626fbf2057fb81af209c1a6d776d23e3d", - "sha256:bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24", - "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", - "sha256:bd3ad3b0a40e713fc68f99ecfd07124195333f1e689387c180813f0e94309d6f", - "sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba", - "sha256:cf28633d64294969c019c6df4ff37f5698e8326db68cc2b66576a51fad634880", - "sha256:d0f35b19894a9e08639fd60a1ec1978cb7f5f7f1eace62f38dd36be8aecdef4d", - "sha256:db1f1c22173ac1c58db249ae48aa7ead29f534b9a948bc56828337aa84a32ed6", - "sha256:dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854", - "sha256:df2f57871a96bbc1b69733cd4c51dc33bea66146b8c63cacbfed73eec0883017", - "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", - "sha256:e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae", - "sha256:e9e0a277bb2eb5d8a7407e14688b85fd8ad628ee4e0c7930415687b6564207a4", - "sha256:ea2bb7e2ae9e37d96835b3576a4fa4b3a97592fbea8ef7c3587078b0068b8f09", - "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", - "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", - "sha256:f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee", - "sha256:f4162988a360a29af158aeb4a2f4f09ffed6a969c9776f8f3bdee9b06a8ab7e5", - "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", - "sha256:f7de08cbe5551911886d1ab60de58448c6df0f67d9feb7d1fb21e9875ef95e91" + "sha256:0255732338c4fdd00996c0421884ea8a3651eea555c3a56b84892b66f696eb70", + "sha256:02f226baeefa68f7d579e213d0f3493496397d8f1cff5e2b222af274c86a552a", + "sha256:059b51b658f4414fff78c6d7b1b4e18283ab5fa56d270ff212d5ba0c561846f4", + "sha256:0bcb1d057b7571334139129b7f941588f69ce7c4ed15a9d6162b2ea54ded700c", + "sha256:0cd48122a6b7eab8f06404805b1bd5856200e3ed6f8a1b9a194f9d9054631beb", + "sha256:19f4718c9012e3baea91a7dba661dcab2451cda2550678dc30d53acb91a7290f", + "sha256:1a161c2c79ab30fe4501d5a2bbfe8b162490757cf90b7f05be8b80bc02f7bb8e", + "sha256:1f4a922da1729f4c40932b2af4fe84909c7a6e167e6e99f71838ce3a29f3fe26", + "sha256:261a1ef047751bb02f29dfe337230b5882b54521ca121fc7f62668133cb119c9", + "sha256:262d23f383170f99cd9191a7c85b9a50970fe9069b2f8ab5d786eca8a675d60b", + "sha256:2ba321813a00e508d5421104464510cc962a6f791aa2fca1c97b1e65027da80d", + "sha256:2c1a1c6ccce4022383583a6ded7bbcda22fc635eb4eb1e0a053336425ed36dfa", + "sha256:352d330048c055ea6db701130abc48a21bec690a8d38f8284e00fab256dc1376", + "sha256:369e0d4647c17c9363244f3468f2227d557a74b6781cb62ce57cf3ef5cc7c610", + "sha256:36ab5b23915887543441efd0417e6a3baa08634308894316f446027611b53bf1", + "sha256:37e32e985f03c06206582a7323ef926b4e78bdaa6915095ef08070471865b906", + "sha256:3a801fef99668f309b88640e28d261991bfad9617c27beda4a3aec4f217ea073", + "sha256:3d14b17b9be5f9c9301f43d2e2a4886a33b53f4e6fdf9ca2f4cc60aeeee76372", + "sha256:422cc684f17bc963da5f59a31530b3936f57c95a29743056ef7a7903a5dbdf88", + "sha256:4520caa3807c1ceb005d125a75e715567806fed67e315cea619d5ec6e75a4191", + "sha256:47834cde750d3c9f4e52c6ca28a7361859fcaf52695c7dc3cc1a720b8922683e", + "sha256:47f9ed103af0bc63182609044b0490747e03bd20a67e391192dde119bf43d52f", + "sha256:498815b96f67dc347e03b719ef49c772589fb74b8ee9ea2c37feae915ad6ebda", + "sha256:54088a5a147ab71a8e7fdfd8c3601972751ded0739c6b696ad9cb0343e21ab73", + "sha256:55f09e00d4dccd76b179c0f18a44f041e5332fd0e022886ba1c0bbf3ea4a18d0", + "sha256:5a0ac90e46fdb5649ab6369d1ab6104bfe5854ab19b645bf5cda0127a13034ae", + "sha256:6411f744f7f20081b1b4e7112e0f4c9c5b08f94b9f086e6f0adf3645f85d3a4d", + "sha256:6413d48a9be53e183eb06495d8e3b006ef8f87c324af68241bbe7a39e8ff54c3", + "sha256:7451f92eddf8503c9b8aa4fe6aa7e87fd51a29c2cfc5f7dbd72efde6c65acf57", + "sha256:8b4c0773b6ada798f51f0f8e30c054d32304ccc6e9c5d93d46cb26f3d385ab19", + "sha256:8dfa94b6a4374e7851bbb6f35e6ded2120b752b063e6acdd3157e4d2bb922eba", + "sha256:97c8425d4e26437e65e1d189d22dff4a079b747ff9c2788057bfb8114ce1e133", + "sha256:9d75f338f5f79ee23548b03d801d28a505198297534f62416391857ea0479571", + "sha256:9de6832228f617c9ef45d948ec1cd8949c482238d68b2477e6f642c33a7b0a54", + "sha256:a4cbdef3ddf777423060c6f81b5694bad2dc9675f110c4b2a60dc0181543fac7", + "sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291", + "sha256:aa70fdbdc3b169d69e8c59e65c07a1c9351ceb438e627f0fdcd471015cd956be", + "sha256:abe38cd8381245a7f49967a6010e77dbf3680bd3627c0fe4362dd693b404c7f8", + "sha256:b13f04968b46ad705f7c8a80122a42ae8f620536ea38cf4bdd374302926424dd", + "sha256:b4ea7e1cff6784e58fe281ce7e7f05036b3e1c89c6f922a6bfbc0a7e8768adbe", + "sha256:b6f91524d31b34f4a5fee24f5bc16dcd1491b668798b6d85585d836c1e633a6a", + "sha256:c26843fd58f65da9491165072da2cccc372530681de481ef670dcc8e27cfb066", + "sha256:c42365005c7a6c42436a54d28c43fe0e01ca11eb2ac3cefe796c25a5f98e5e9b", + "sha256:c8b82a55ef86a2d8e81b63da85e55f5537d2157165be1cb2ce7cfa57b6aef38b", + "sha256:ced69262a8278547e63409b2653b372bf4baff0870c57efa76c5703fd6543282", + "sha256:d2e3bdadaba0e040d1e7ab39db73e0afe2c74ae277f5614dad53eadbecbbb169", + "sha256:d403c84991b5ad291d3809bace5e85f4bbf44a04bdc9a88ed2bb1807b3360bb8", + "sha256:d7543263084a85fbc09c704b515395398d31d6395518446237eac219eab9e55e", + "sha256:d8882a829fd779f0f43998e931c466802a77ca1ee0fe25a3abe50278616b1471", + "sha256:e4f0b035d9d0ed519c813ee23e0a733db81ec37d2e9503afbb6e54ccfdee0fa7", + "sha256:e8b025c351b9f0e8b5436cf28a07fa4ac0204d67b38f01433ac7f9b870fa38c6", + "sha256:eb7fd5b184e5d277afa9ec0ad5e4eb562ecff541e7f60e69ee69c8d59e9aeaba", + "sha256:ec31367fd6a255dc8de4772bd1658c3e926d8e860a0b6e922b615e532d320ddc", + "sha256:ee461a4eaab4f165b68780a6a1af95fb23a29932be7569b9fab666c407969051", + "sha256:f5045039100ed58fa817a6227a356240ea1b9a1bc141018864c306c1a16d4175" ], "markers": "python_version >= '3.10'", - "version": "==2.2.4" + "version": "==2.2.5" }, "packaging": { "hashes": [ - "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", - "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" ], "markers": "python_version >= '3.8'", - "version": "==24.2" + "version": "==25.0" }, "pathvalidate": { "hashes": [ @@ -1149,11 +1149,11 @@ }, "setuptools": { "hashes": [ - "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", - "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8" + "sha256:5a78f61820bc088c8e4add52932ae6b8cf423da2aff268c23f813cfbb13b4006", + "sha256:6cdc8cb9a7d590b237dbe4493614a9b75d0559b888047c1f67d49ba50fc3edb2" ], "markers": "python_version >= '3.9'", - "version": "==78.1.0" + "version": "==80.4.0" }, "shapely": { "hashes": [ @@ -1498,11 +1498,11 @@ }, "certifi": { "hashes": [ - "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", - "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" + "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", + "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3" ], "markers": "python_version >= '3.6'", - "version": "==2025.1.31" + "version": "==2025.4.26" }, "cfgv": { "hashes": [ @@ -1514,109 +1514,109 @@ }, "charset-normalizer": { "hashes": [ - "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", - "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", - "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", - "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", - "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", - "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", - "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", - "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", - "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", - "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", - "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", - "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", - "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", - "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", - "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", - "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", - "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", - "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", - "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", - "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", - "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", - "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", - "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", - "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", - "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", - "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", - "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", - "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", - "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", - "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", - "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", - "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", - "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", - "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", - "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", - "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", - "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", - "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", - "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", - "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", - "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", - "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", - "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", - "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", - "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", - "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", - "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", - "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", - "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", - "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", - "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", - "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", - "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", - "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", - "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", - "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", - "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", - "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", - "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", - "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", - "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", - "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", - "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", - "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", - "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", - "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", - "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", - "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", - "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", - "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", - "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", - "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", - "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", - "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", - "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", - "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", - "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", - "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", - "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", - "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", - "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", - "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", - "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", - "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", - "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", - "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", - "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", - "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", - "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", - "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", - "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", - "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" + "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", + "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", + "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", + "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", + "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", + "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", + "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", + "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", + "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", + "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", + "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", + "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", + "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", + "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", + "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", + "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", + "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", + "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", + "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", + "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", + "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", + "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", + "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", + "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", + "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", + "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", + "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", + "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", + "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", + "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", + "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", + "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", + "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", + "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", + "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", + "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", + "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", + "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", + "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", + "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", + "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", + "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", + "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", + "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", + "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", + "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", + "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", + "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", + "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", + "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", + "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", + "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", + "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", + "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", + "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", + "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", + "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", + "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", + "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", + "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", + "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", + "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", + "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", + "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", + "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", + "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", + "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", + "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", + "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", + "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", + "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", + "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", + "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", + "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", + "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", + "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", + "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", + "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", + "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", + "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", + "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", + "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", + "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", + "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", + "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", + "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", + "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", + "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", + "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", + "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", + "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", + "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" ], "markers": "python_version >= '3.7'", - "version": "==3.4.1" + "version": "==3.4.2" }, "click": { "hashes": [ - "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", - "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", + "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.8" + "markers": "python_version >= '3.10'", + "version": "==8.2.0" }, "coverage": { "extras": [ @@ -1707,11 +1707,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", - "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" + "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", + "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88" ], "markers": "python_version < '3.11'", - "version": "==1.2.2" + "version": "==1.3.0" }, "filelock": { "hashes": [ @@ -1723,11 +1723,11 @@ }, "identify": { "hashes": [ - "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", - "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf" + "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", + "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25" ], "markers": "python_version >= '3.9'", - "version": "==2.6.9" + "version": "==2.6.10" }, "idna": { "hashes": [ @@ -1763,11 +1763,11 @@ }, "mypy-extensions": { "hashes": [ - "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", - "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", + "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" ], - "markers": "python_version >= '3.5'", - "version": "==1.0.0" + "markers": "python_version >= '3.8'", + "version": "==1.1.0" }, "nodeenv": { "hashes": [ @@ -1779,11 +1779,11 @@ }, "packaging": { "hashes": [ - "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", - "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" ], "markers": "python_version >= '3.8'", - "version": "==24.2" + "version": "==25.0" }, "pathspec": { "hashes": [ @@ -1795,11 +1795,11 @@ }, "platformdirs": { "hashes": [ - "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", - "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351" + "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", + "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4" ], "markers": "python_version >= '3.9'", - "version": "==4.3.7" + "version": "==4.3.8" }, "pluggy": { "hashes": [ @@ -2017,11 +2017,11 @@ }, "virtualenv": { "hashes": [ - "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", - "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6" + "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", + "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af" ], "markers": "python_version >= '3.8'", - "version": "==20.30.0" + "version": "==20.31.2" } } } diff --git a/server/mergin/auth/models.py b/server/mergin/auth/models.py index 3447d654..c6ad860a 100644 --- a/server/mergin/auth/models.py +++ b/server/mergin/auth/models.py @@ -7,7 +7,7 @@ from typing import List, Optional import bcrypt import re -from flask import current_app, request +from flask import current_app, request, abort from sqlalchemy import or_, func, text from ..app import db @@ -51,6 +51,8 @@ def __repr__(self): return "" % self.username def check_password(self, password): + if self.passwd is None: + abort(401, "Login with password is not possible") if isinstance(password, str): password = password.encode("utf-8") return bcrypt.checkpw(password, self.passwd.encode("utf-8")) diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index 2836c7cf..3e68c86c 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -943,3 +943,18 @@ def test_email_format_validation(app, username, is_valid): data={"email": email} ) # ResetPasswordForm has only email field assert form.validate() == is_valid + + +def test_login_without_password(client): + """Test password check as SSO user which does not have password""" + login_as_admin(client) + username = "sso_user" + user = add_user(username=username, password="") + assert user.username == username + for pwd in ["Il0vemergin", ""]: + resp = client.post( + url_for("/.mergin_auth_controller_login"), + data=json.dumps({"login": username, "password": pwd}), + headers=json_headers, + ) + assert resp.status_code == 401 From ab79d97f566827cb82fcf03a73e3fd1d49247690 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Mon, 26 May 2025 11:05:21 +0200 Subject: [PATCH 15/17] Do not use abort in models --- server/mergin/auth/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/mergin/auth/models.py b/server/mergin/auth/models.py index c6ad860a..39b94e91 100644 --- a/server/mergin/auth/models.py +++ b/server/mergin/auth/models.py @@ -7,7 +7,7 @@ from typing import List, Optional import bcrypt import re -from flask import current_app, request, abort +from flask import current_app, request from sqlalchemy import or_, func, text from ..app import db @@ -51,8 +51,9 @@ def __repr__(self): return "" % self.username def check_password(self, password): + # users created through SSO if self.passwd is None: - abort(401, "Login with password is not possible") + return if isinstance(password, str): password = password.encode("utf-8") return bcrypt.checkpw(password, self.passwd.encode("utf-8")) From a99bc7af68756266e6bfd9e71f70a9384016cc04 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Tue, 27 May 2025 09:59:08 +0200 Subject: [PATCH 16/17] Make test closer to real behaviour --- server/mergin/auth/models.py | 2 +- server/mergin/tests/test_auth.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/mergin/auth/models.py b/server/mergin/auth/models.py index 39b94e91..a56aa162 100644 --- a/server/mergin/auth/models.py +++ b/server/mergin/auth/models.py @@ -52,7 +52,7 @@ def __repr__(self): def check_password(self, password): # users created through SSO - if self.passwd is None: + if not self.passwd: return if isinstance(password, str): password = password.encode("utf-8") diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index 3e68c86c..79c43902 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -950,6 +950,8 @@ def test_login_without_password(client): login_as_admin(client) username = "sso_user" user = add_user(username=username, password="") + user.passwd = None + db.session.commit() assert user.username == username for pwd in ["Il0vemergin", ""]: resp = client.post( From 0136d12c73f95c77c8436d95b3e475cc9fa496d5 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Tue, 27 May 2025 10:20:17 +0200 Subject: [PATCH 17/17] revert back --- server/mergin/auth/models.py | 2 +- server/mergin/tests/test_auth.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/server/mergin/auth/models.py b/server/mergin/auth/models.py index a56aa162..39b94e91 100644 --- a/server/mergin/auth/models.py +++ b/server/mergin/auth/models.py @@ -52,7 +52,7 @@ def __repr__(self): def check_password(self, password): # users created through SSO - if not self.passwd: + if self.passwd is None: return if isinstance(password, str): password = password.encode("utf-8") diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index 79c43902..3a4c2de4 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -950,9 +950,7 @@ def test_login_without_password(client): login_as_admin(client) username = "sso_user" user = add_user(username=username, password="") - user.passwd = None - db.session.commit() - assert user.username == username + assert user.passwd is None for pwd in ["Il0vemergin", ""]: resp = client.post( url_for("/.mergin_auth_controller_login"),