Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions deployment/community/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -215,5 +213,7 @@ NO_MONKEY_PATCH=False

# Diagnostic logs

DIAGNOSTIC_LOGS_URL=

DIAGNOSTIC_LOGS_DIR=/diagnostic_logs

4 changes: 0 additions & 4 deletions deployment/community/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ services:
volumes:
- ./projects:/data
- ./diagnostic_logs:/diagnostic_logs
- ../common/entrypoint.sh:/app/entrypoint.sh
env_file:
- .prod.env
depends_on:
Expand All @@ -48,8 +47,6 @@ services:
environment:
- GEVENT_WORKER=0
- NO_MONKEY_PATCH=1
volumes:
- ../common/entrypoint.sh:/app/entrypoint.sh
depends_on:
- redis
- server
Expand All @@ -68,7 +65,6 @@ services:
- NO_MONKEY_PATCH=1
volumes:
- ./projects:/data
- ../common/entrypoint.sh:/app/entrypoint.sh
depends_on:
- redis
- server
Expand Down
7 changes: 3 additions & 4 deletions deployment/enterprise/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 2 additions & 4 deletions deployment/enterprise/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions server/mergin/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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:
Expand All @@ -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}
2 changes: 1 addition & 1 deletion server/mergin/sync/public_api_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions server/mergin/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion server/mergin/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@


def get_version():
return "2025.6.2"
return "2025.7.3"
Original file line number Diff line number Diff line change
@@ -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")
2 changes: 1 addition & 1 deletion server/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading