diff --git a/README.md b/README.md index 6fbc30d3..5d3a912b 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ You are currently browsing repository for Mergin Maps web server and web client. - 🌱 **Sharing with collaborators** - Projects can be shared with other team members - 🏰 **Permission system** - Decide who can read, write or manage projects - 🌈 **Web interface** - Simple user interface to view and manage projects -- ⚡️ **Fast** - Efficient sync protocol transfering data between clients and server +- ⚡️ **Fast** - Efficient sync protocol transferring data between clients and server - 🧑‍💻 **Developer friendly** - Mergin Maps is open platform. CLI tools and client libraries are available for [Python](https://github.com/MerginMaps/python-api-client) and [C++](https://github.com/MerginMaps/cpp-api-client) - :camera: **Sync images** - Supporting sync of photos with common cloud storage using [mergin-media-sync](https://github.com/MerginMaps/media-sync) tool - 💽 **Sync with database** - Supporting two-way sync of data with PostGIS using [mergin-db-sync](https://github.com/MerginMaps/db-sync) tool @@ -74,7 +74,7 @@ Admin users can enter the admin interface available at `/admin` URL which provid ### Contributing -Contributions are welcomed! You can set up development environment by following a guide in [development.md](./deployment/community/development.md). Before you create your first pull request, we kindly ask you to sign the CLA with your GitHub user name and date [here](LICENSES/CLA-signed-list.md). +Contributions are welcomed! You can set up development environment by following a guide in [development.md](./development.md). Before you create your first pull request, we kindly ask you to sign the CLA with your GitHub user name and date [here](LICENSES/CLA-signed-list.md). ## Documentation @@ -93,7 +93,7 @@ If you need support, a custom deployment, extending the service capabilities and Contributions are welcome! -More information for developers can be found in the dedicated [development](./deployment/community/development.md) page. +More information for developers can be found in the dedicated [development](./development.md) page. Client side modules: - [Python](https://github.com/MerginMaps/python-api-client) client library + CLI diff --git a/deployment/community/.env.template b/deployment/community/.env.template index 4526246e..86e4a6b0 100644 --- a/deployment/community/.env.template +++ b/deployment/community/.env.template @@ -98,12 +98,10 @@ MAIL_SUPPRESS_SEND=0 #MAIL_BCC='' -#MERGIN_LOGO_URL= # for link to logo in emails +MERGIN_LOGO_URL=https://merginmaps.com/MM_logo_HORIZ_COLOR_TRANSPARENT_no_padding.png #MAIL_DEBUG=MAIL_SUPPRESS_SEND | False - - # data sync #LOCAL_PROJECTS=os.path.join(config_dir, os.pardir, os.pardir, 'projects') # for local storage type @@ -215,5 +213,7 @@ NO_MONKEY_PATCH=False # Diagnostic logs +DIAGNOSTIC_LOGS_URL= + DIAGNOSTIC_LOGS_DIR=/diagnostic_logs diff --git a/deployment/community/docker-compose.yml b/deployment/community/docker-compose.yml index 8b5d5156..3a5b2665 100644 --- a/deployment/community/docker-compose.yml +++ b/deployment/community/docker-compose.yml @@ -30,7 +30,6 @@ services: volumes: - ./projects:/data - ./diagnostic_logs:/diagnostic_logs - - ../common/entrypoint.sh:/app/entrypoint.sh env_file: - .prod.env depends_on: @@ -48,8 +47,6 @@ services: environment: - GEVENT_WORKER=0 - NO_MONKEY_PATCH=1 - volumes: - - ../common/entrypoint.sh:/app/entrypoint.sh depends_on: - redis - server @@ -68,7 +65,6 @@ services: - NO_MONKEY_PATCH=1 volumes: - ./projects:/data - - ../common/entrypoint.sh:/app/entrypoint.sh depends_on: - redis - server diff --git a/deployment/enterprise/.env.template b/deployment/enterprise/.env.template index f51c8bef..663ba5ac 100644 --- a/deployment/enterprise/.env.template +++ b/deployment/enterprise/.env.template @@ -175,11 +175,8 @@ ACCOUNT_EXPIRATION=1 # for links generated in emails -#MERGIN_BASE_URL=http://localhost:5000 -MERGIN_BASE_URL=fixme - #MERGIN_LOGO_URL= # for link to logo in emails -MERGIN_LOGO_URL=fixme +MERGIN_LOGO_URL=https://merginmaps.com/MM_logo_HORIZ_COLOR_TRANSPARENT_no_padding.png # global workspace related bits - ignored in non-CE versions # GLOBAL_WORKSPACE mergin @@ -228,6 +225,8 @@ VECTOR_TILES_STYLE_URL=https://tiles-ee.merginmaps.com//styles/default.json ### Diagnostic logs from Mobile and QGIS Plugin DIAGNOSTIC_LOGS_DIR=/diagnostic_logs +DIAGNOSTIC_LOGS_URL= + ### SSO ################################################################################################################ SSO_ENABLED=False diff --git a/deployment/enterprise/docker-compose.yml b/deployment/enterprise/docker-compose.yml index c4524707..902a66ef 100644 --- a/deployment/enterprise/docker-compose.yml +++ b/deployment/enterprise/docker-compose.yml @@ -13,9 +13,10 @@ services: 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 + environment: + - GUNICORN_CMD_ARGS="--limit-request-line 8190" depends_on: - db networks: @@ -56,8 +57,6 @@ services: restart: always user: 901:999 command: ["celery -A application.celery beat --loglevel=info"] - volumes: - - ../common/entrypoint.sh:/app/entrypoint.sh env_file: - .prod.env depends_on: @@ -75,7 +74,6 @@ services: volumes: - ./data:/data # map data dir to host - ./map_data:/overviews - - ../common/entrypoint.sh:/app/entrypoint.sh env_file: - .prod.env depends_on: diff --git a/development.md b/development.md index b2a1be08..2790f089 100644 --- a/development.md +++ b/development.md @@ -43,7 +43,7 @@ $ yarn install $ yarn link:dependencies # link dependencies $ yarn build:libs # bild libraries @mergin/lib @mergin/admin-lib @mergin/lib-vue2 $ yarn dev # development client web application dev server on port 8080 (package @mergin/app) -$ yarn dev:admin # development admin appplication dev server on port 8081 (package @mergin/admin-app) +$ yarn dev:admin # development admin application dev server on port 8081 (package @mergin/admin-app) ``` If you are developing a library package (named **-lib*), it is useful to watch the library for changes instead of rebuilding it each time. @@ -98,7 +98,7 @@ docker exec -it merginmaps-server flask server send-check-email --email admin@e In docker-compose.dev.yml is started maildev/maildev image that can be used to test emails (see [https://github.com/maildev/maildev/](https://github.com/maildev/maildev/)). In localhost:1080 you can see the emails sent by the application in web interface. ### Running with remote debugger -If you want to run the application with remote debugger, you can use debug compose file with attatched source code and reload. +If you want to run the application with remote debugger, you can use debug compose file with attached source code and reload. It starts a debugpy session on port 5678 you can attach to. ```shell diff --git a/server/mergin/auth/models.py b/server/mergin/auth/models.py index 39b94e91..5dcf275e 100644 --- a/server/mergin/auth/models.py +++ b/server/mergin/auth/models.py @@ -36,6 +36,8 @@ class User(db.Model): default=datetime.datetime.utcnow, ) + last_signed_in = db.Column(db.DateTime(), nullable=True) + __table_args__ = ( db.Index("ix_user_username", func.lower(username), unique=True), db.Index("ix_user_email", func.lower(email), unique=True), @@ -289,6 +291,7 @@ def __init__(self, user_id: int, ua: str, ip: str, device_id: Optional[str] = No self.user_agent = ua self.ip_address = ip self.device_id = device_id + self.timestamp = datetime.datetime.now(tz=datetime.timezone.utc) @staticmethod def add_record(user_id: int, req: request) -> None: @@ -300,4 +303,37 @@ def add_record(user_id: int, req: request) -> None: return lh = LoginHistory(user_id, ua, ip, device_id) db.session.add(lh) + + # cache user last login + User.query.filter_by(id=user_id).update({"last_signed_in": lh.timestamp}) + db.session.commit() + + @staticmethod + def get_users_last_signed_in(user_ids: list) -> dict: + """Get users last signed in dates. + Result is also cached in User table for future use. + """ + result = ( + db.session.query( + LoginHistory.user_id, + func.max(LoginHistory.timestamp).label("last_signed_in"), + ) + .filter(LoginHistory.user_id.in_(user_ids)) + .group_by(LoginHistory.user_id) + .all() + ) + + user_mapping = [ + { + "id": row.user_id, # user_id as PK in User table + "last_signed_in": row.last_signed_in, + } + for row in result + ] + if not user_mapping: + return {} + + # cache users last signed in + db.session.bulk_update_mappings(User, user_mapping) db.session.commit() + return {item["id"]: item["last_signed_in"] for item in user_mapping} diff --git a/server/mergin/sync/public_api_controller.py b/server/mergin/sync/public_api_controller.py index 9fd229a1..a82c5768 100644 --- a/server/mergin/sync/public_api_controller.py +++ b/server/mergin/sync/public_api_controller.py @@ -1021,7 +1021,7 @@ def push_finish(transaction_id): ) corrupted_files.append(f.path) continue - if not is_supported_type(dest_file): + if not f.diff and not is_supported_type(dest_file): logging.info(f"Rejecting blacklisted file: {dest_file}") abort( 400, diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index 90777122..d53b01bc 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -77,6 +77,15 @@ def test_login(client, data, headers, expected): ) assert login_history assert login_history.device_id == str(headers.get("X-Device-Id")) + assert user.last_signed_in == login_history.timestamp + users_last_signed_in = LoginHistory.get_users_last_signed_in([user.id]) + assert users_last_signed_in[user.id] == login_history.timestamp + + # verify missing value is cached on first LoginHistory access + user.last_signed_in = None + db.session.commit() + users_last_signed_in = LoginHistory.get_users_last_signed_in([user.id]) + assert user.last_signed_in == users_last_signed_in[user.id] def test_logout(client): @@ -376,6 +385,7 @@ def test_api_login(client, data, headers, expected): .first() ) assert login_history + assert user.last_signed_in == login_history.timestamp def test_api_login_from_urllib(client): @@ -394,6 +404,8 @@ def test_api_login_from_urllib(client): .first() ) assert not login_history + # we do not have recored last login yet + assert user.last_signed_in is None def test_api_user_profile(client): diff --git a/server/mergin/version.py b/server/mergin/version.py index 214212f7..cdacf710 100644 --- a/server/mergin/version.py +++ b/server/mergin/version.py @@ -4,4 +4,4 @@ def get_version(): - return "2025.6.2" + return "2025.7.3" diff --git a/server/migrations/community/b9ec9ab6694f_add_user_last_signed_in.py b/server/migrations/community/b9ec9ab6694f_add_user_last_signed_in.py new file mode 100644 index 00000000..0ebc9250 --- /dev/null +++ b/server/migrations/community/b9ec9ab6694f_add_user_last_signed_in.py @@ -0,0 +1,25 @@ +"""Add user last signed in + +Revision ID: b9ec9ab6694f +Revises: 6cb54659c1de +Create Date: 2025-09-09 15:43:19.554498 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "b9ec9ab6694f" +down_revision = "6cb54659c1de" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column("user", sa.Column("last_signed_in", sa.DateTime(), nullable=True)) + + +def downgrade(): + op.drop_column("user", "last_signed_in") diff --git a/server/setup.py b/server/setup.py index 2abd701d..33f41ea7 100644 --- a/server/setup.py +++ b/server/setup.py @@ -6,7 +6,7 @@ setup( name="mergin", - version="2025.6.2", + version="2025.7.3", url="https://github.com/MerginMaps/mergin", license="AGPL-3.0-only", author="Lutra Consulting Limited",