Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 3 additions & 4 deletions server/mergin/sync/project_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .permissions import ProjectPermissions
from sqlalchemy import or_, and_
from typing import List
from ..auth.models import User, UserProfile
from ..auth.models import User


class ProjectHandler(AbstractProjectHandler):
Expand All @@ -12,8 +12,7 @@ def get_push_permission(self, changes: dict):

def get_email_receivers(self, project: Project) -> List[User]:
return (
User.query.join(UserProfile)
.outerjoin(ProjectUser, ProjectUser.user_id == User.id)
User.query.outerjoin(ProjectUser, ProjectUser.user_id == User.id)
.filter(
or_(
and_(
Expand All @@ -24,7 +23,7 @@ def get_email_receivers(self, project: Project) -> List[User]:
),
User.active,
User.verified_email,
UserProfile.receive_notifications,
User.receive_notifications,
)
.all()
)
Expand Down
3 changes: 1 addition & 2 deletions server/mergin/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..auth.bearer import decode_token, encode_token
from ..auth.forms import ResetPasswordForm
from ..auth.app import generate_confirmation_token, confirm_token
from ..auth.models import User, UserProfile, LoginHistory
from ..auth.models import User, LoginHistory
from ..auth.tasks import anonymize_removed_users
from ..app import db
from ..sync.models import Project, ProjectRole
Expand Down Expand Up @@ -286,7 +286,6 @@ def test_change_password(client):
email="user_test@mergin.com",
)
user.active = True
user.profile = UserProfile()
db.session.add(user)
db.session.commit()

Expand Down
3 changes: 1 addition & 2 deletions server/mergin/tests/test_project_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from ..sync.files import files_changes_from_upload
from ..sync.schemas import ProjectListSchema
from ..sync.utils import Checkpoint, generate_checksum, is_versioned_file
from ..auth.models import User, UserProfile
from ..auth.models import User

from . import (
test_project,
Expand Down Expand Up @@ -666,7 +666,6 @@ def test_update_project(client):
username="tester", passwd="tester", is_admin=False, email="tester@mergin.com"
)
test_user.active = True
test_user.profile = UserProfile()
db.session.add(test_user)
db.session.commit()

Expand Down
3 changes: 1 addition & 2 deletions server/mergin/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from dateutil.tz import tzlocal
from pygeodiff import GeoDiff

from ..auth.models import User, UserProfile
from ..auth.models import User
from ..sync.utils import generate_location, generate_checksum
from ..sync.models import (
Project,
Expand Down Expand Up @@ -52,7 +52,6 @@ def add_user(username="random", password="random", is_admin=False) -> User:
)
user.active = True
user.verified_email = True
user.profile = UserProfile()
db.session.add(user)
db.session.commit()
return user
Expand Down
Loading
Loading