Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
df948a7
Change get users logic
xkello Apr 11, 2026
71bb92f
Replace lockfile with upload last_ping db column
varmar05 Apr 14, 2026
dde191f
Rework concurrent upload using upsert strategy
varmar05 Apr 14, 2026
52ad8ef
Merge user_profile into user table
varmar05 Apr 14, 2026
d9809a5
Fix migration
varmar05 Apr 15, 2026
72f4a62
Merge pull request #611 from MerginMaps/merge_user_profile
MarcelGeo Apr 15, 2026
f9e6fc8
Merge branch 'develop' into rework_concurrent_upload
varmar05 Apr 16, 2026
b537864
Update revision branch hash
varmar05 Apr 16, 2026
a0ac896
Merge pull request #609 from MerginMaps/implement-#3263
MarcelGeo Apr 16, 2026
257e16d
Concurrent upload fixes
varmar05 Apr 17, 2026
683eaf7
Fix: Swap file rename and DB project version commit to avoid orphaned…
varmar05 Apr 17, 2026
ad99574
Merge pull request #610 from MerginMaps/rework_concurrent_upload
MarcelGeo Apr 28, 2026
19ee910
Merge pull request #614 from MerginMaps/master
varmar05 Apr 29, 2026
5a7129e
Fix upload migrations and merge into single transaction
varmar05 Apr 30, 2026
0d13e2b
Merge pull request #615 from MerginMaps/fix_upload_migration
varmar05 Apr 30, 2026
3c42ce9
fix failing test
varmar05 Apr 30, 2026
e95017f
Merge pull request #616 from MerginMaps/fix_tests
varmar05 Apr 30, 2026
5c5cbdd
Handle rename error in case of full disk
varmar05 May 5, 2026
d444d7b
Merge pull request #619 from MerginMaps/handle_rename_error
MarcelGeo May 5, 2026
d14b8fa
Bump 2026.4.0
MarcelGeo May 6, 2026
bcf10cc
Merge pull request #621 from MerginMaps/bump-2026.4.0
MarcelGeo May 6, 2026
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
3 changes: 1 addition & 2 deletions server/mergin/auth/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from sqlalchemy import or_, func

from ..app import db
from .models import User, UserProfile
from .models import User
from ..commands import normalize_input


Expand Down Expand Up @@ -36,7 +36,6 @@ def create(username, password, is_admin, email): # pylint: disable=W0612
sys.exit(1)

user = User(username=username, passwd=password, is_admin=is_admin, email=email)
user.profile = UserProfile()
user.active = True
db.session.add(user)
db.session.commit()
Expand Down
15 changes: 8 additions & 7 deletions server/mergin/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
CANNOT_EDIT_PROFILE_MSG,
)
from .bearer import encode_token
from .models import User, LoginHistory, UserProfile
from .models import User, LoginHistory
from .schemas import UserSchema, UserSearchSchema, UserProfileSchema, UserInfoSchema
from .forms import (
LoginForm,
Expand Down Expand Up @@ -65,7 +65,7 @@ def user_profile(user, return_all=True):
{
"email": user.email,
"storage_limit": data["storage"], # duplicate - we should remove it
"receive_notifications": user.profile.receive_notifications,
"receive_notifications": user.receive_notifications,
"verified_email": user.verified_email,
"tier": "free",
"registration_date": user.registration_date,
Expand Down Expand Up @@ -369,7 +369,6 @@ def update_user_profile(): # pylint: disable=W0613,W0612
return jsonify(form.errors), 400
current_user.verified_email = False

form.update_obj(current_user.profile)
form.update_obj(current_user)
db.session.add(current_user)
db.session.commit()
Expand Down Expand Up @@ -483,22 +482,24 @@ def get_paginated_users(

:rtype: Dict[str: List[User], str: Integer]
"""
users = User.query.join(UserProfile).filter(
users = User.query.filter(
is_(User.username.ilike("deleted_%"), False) | is_(User.active, True)
)

if like:
users = users.filter(
User.username.ilike(f"%{like}%")
| User.email.ilike(f"%{like}%")
| UserProfile.first_name.ilike(f"%{like}%")
| UserProfile.last_name.ilike(f"%{like}%")
| User.first_name.ilike(f"%{like}%")
| User.last_name.ilike(f"%{like}%")
)

if descending and order_by:
users = users.order_by(desc(User.__table__.c[order_by]))
elif not descending and order_by:
users = users.order_by(asc(User.__table__.c[order_by]))
else:
users = users.order_by(asc(User.id))

paginate = users.paginate(page=page, per_page=per_page)
result = paginate.items
Expand Down Expand Up @@ -561,7 +562,7 @@ def create_user():
workspace_role=request.json["role"],
)

if user.profile.receive_notifications:
if user.receive_notifications:
send_confirmation_email(
current_app,
user,
Expand Down
43 changes: 16 additions & 27 deletions server/mergin/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@

class User(db.Model):
id = db.Column(db.Integer, primary_key=True)

username = db.Column(db.String(80), info={"label": "Username"})
email = db.Column(db.String(120))

passwd = db.Column(db.String(80), info={"label": "Password"}) # salted + hashed

active = db.Column(db.Boolean, default=True)
is_admin = db.Column(db.Boolean)
verified_email = db.Column(db.Boolean, default=False)
Expand All @@ -35,8 +32,12 @@ class User(db.Model):
info={"label": "Date of creation of user account"},
default=datetime.datetime.utcnow,
)

last_signed_in = db.Column(db.DateTime(), nullable=True)
receive_notifications = db.Column(
db.Boolean, default=True, nullable=False, index=True
)
first_name = db.Column(db.String(256), nullable=True)
last_name = db.Column(db.String(256), nullable=True)

__table_args__ = (
db.Index("ix_user_username", func.lower(username), unique=True),
Expand Down Expand Up @@ -187,8 +188,8 @@ def anonymize(self):
self.username = del_str
self.email = None
self.passwd = None
self.profile.first_name = None
self.profile.last_name = None
self.first_name = None
self.last_name = None
db.session.commit()

@classmethod
Expand Down Expand Up @@ -240,38 +241,26 @@ def create(
cls, username: str, email: str, password: str, notifications: bool = True
) -> User:
user = cls(username.strip(), email.strip(), password, False)
user.profile = UserProfile(receive_notifications=notifications)
user.receive_notifications = notifications
db.session.add(user)
db.session.commit()
return user

@property
def profile(self) -> "User":
"""Compatibility shim: profile fields are now on User directly."""
return self

def name(self) -> Optional[str]:
return f'{self.first_name if self.first_name else ""} {self.last_name if self.last_name else ""}'.strip()

@property
def can_edit_profile(self) -> bool:
"""Flag if we allow user to edit their email and name"""
# False when user is created by SSO login
return self.passwd is not None and self.active


class UserProfile(db.Model):
user_id = db.Column(
db.Integer, db.ForeignKey("user.id", ondelete="CASCADE"), primary_key=True
)
receive_notifications = db.Column(db.Boolean, default=True, index=True)
first_name = db.Column(db.String(256), nullable=True, info={"label": "First name"})
last_name = db.Column(db.String(256), nullable=True, info={"label": "Last name"})

user = db.relationship(
"User",
uselist=False,
backref=db.backref(
"profile", single_parent=True, uselist=False, cascade="all,delete"
),
)

def name(self) -> Optional[str]:
return f'{self.first_name if self.first_name else ""} {self.last_name if self.last_name else ""}'.strip()


class LoginHistory(db.Model):
id = db.Column(db.Integer, primary_key=True)
timestamp = db.Column(db.DateTime(), default=datetime.datetime.utcnow, index=True)
Expand Down
33 changes: 21 additions & 12 deletions server/mergin/auth/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from flask import current_app
from marshmallow import fields

from .models import User, UserProfile
from .models import User
from ..app import DateTimeWithZ, ma


Expand All @@ -20,35 +20,44 @@ class UserProfileSchema(ma.SQLAlchemyAutoSchema):

def get_storage(self, obj):
# DEPRECATED functionality - kept for the backward-compatibility
ws = current_app.ws_handler.get_by_name(obj.user.username)
ws = current_app.ws_handler.get_by_name(obj.username)
if ws:
return ws.storage

def get_disk_usage(self, obj):
# DEPRECATED functionality - kept for the backward-compatibility
ws = current_app.ws_handler.get_by_name(obj.user.username)
ws = current_app.ws_handler.get_by_name(obj.username)
if ws:
return ws.disk_usage()

def _has_project(self, obj):
# DEPRECATED functionality - kept for the backward-compatibility
from ..sync.models import ProjectUser, Project

ws = current_app.ws_handler.get_by_name(obj.user.username)
ws = current_app.ws_handler.get_by_name(obj.username)
if ws:
projects_count = (
Project.query.join(ProjectUser)
.filter(Project.creator_id == obj.user.id)
.filter(Project.creator_id == obj.id)
.filter(Project.removed_at.is_(None))
.filter(Project.workspace_id == ws.id)
.filter(ProjectUser.user_id == obj.user.id)
.filter(ProjectUser.user_id == obj.id)
.count()
)
return projects_count > 0
return False

class Meta:
model = UserProfile
model = User
fields = (
"receive_notifications",
"first_name",
"last_name",
"name",
"storage",
"disk_usage",
"has_project",
)
load_instance = True


Expand Down Expand Up @@ -81,7 +90,7 @@ class UserSearchSchema(ma.SQLAlchemyAutoSchema):
name = fields.Method("_name", dump_only=True)

def _name(self, obj):
return obj.profile.name()
return obj.name()

class Meta:
model = User
Expand All @@ -97,11 +106,11 @@ class Meta:
class UserInfoSchema(ma.SQLAlchemyAutoSchema):
"""User schema with full information"""

first_name = fields.String(attribute="profile.first_name")
last_name = fields.String(attribute="profile.last_name")
receive_notifications = fields.Boolean(attribute="profile.receive_notifications")
first_name = fields.String()
last_name = fields.String()
receive_notifications = fields.Boolean()
registration_date = DateTimeWithZ(attribute="registration_date")
name = fields.Function(lambda obj: obj.profile.name())
name = fields.Function(lambda obj: obj.name())
can_edit_profile = fields.Boolean(attribute="can_edit_profile")

class Meta:
Expand Down
3 changes: 1 addition & 2 deletions server/mergin/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def init_db():
)
def init(email: str, recreate: bool):
"""Initialize database if does not exist or -r is provided. Perform check of server configuration. Send statistics, respecting your setup."""
from .auth.models import User, UserProfile
from .auth.models import User

inspect_engine = inspect(db.engine)
tables = inspect_engine.get_table_names()
Expand All @@ -221,7 +221,6 @@ def init(email: str, recreate: bool):
password_chars = string.ascii_letters + string.digits
password = "".join(random.choice(password_chars) for i in range(12))
user = User(username=username, passwd=password, email=email, is_admin=True)
user.profile = UserProfile()
user.active = True
db.session.add(user)
db.session.commit()
Expand Down
Loading
Loading