diff --git a/server/mergin/auth/api.yaml b/server/mergin/auth/api.yaml index e0771689..fa482a36 100644 --- a/server/mergin/auth/api.yaml +++ b/server/mergin/auth/api.yaml @@ -753,6 +753,11 @@ components: type: string format: date-time example: 2023-07-30T08:47:58Z + last_signed_in: + nullable: true + type: string + format: date-time + example: 2025-12-18T08:47:58Z profile: $ref: "#/components/schemas/UserProfile" PaginatedUsers: diff --git a/server/mergin/auth/models.py b/server/mergin/auth/models.py index 5dcf275e..470b934b 100644 --- a/server/mergin/auth/models.py +++ b/server/mergin/auth/models.py @@ -268,7 +268,7 @@ class UserProfile(db.Model): ), ) - def name(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() diff --git a/server/mergin/auth/schemas.py b/server/mergin/auth/schemas.py index 3f614ae1..52ed01f6 100644 --- a/server/mergin/auth/schemas.py +++ b/server/mergin/auth/schemas.py @@ -11,7 +11,7 @@ class UserProfileSchema(ma.SQLAlchemyAutoSchema): name = ma.Function( - lambda obj: f'{obj.first_name if obj.first_name else ""} {obj.last_name if obj.last_name else ""}'.strip(), + lambda obj: obj.name(), dump_only=True, ) storage = fields.Method("get_storage", dump_only=True) @@ -70,6 +70,7 @@ class Meta: "profile", "scheduled_removal", "registration_date", + "last_signed_in", ) load_instance = True diff --git a/server/mergin/sync/models.py b/server/mergin/sync/models.py index 9574a69d..2430c05e 100644 --- a/server/mergin/sync/models.py +++ b/server/mergin/sync/models.py @@ -304,6 +304,7 @@ def get_member(self, user_id: int) -> Optional[ProjectMember]: project_role=ProjectRole(member.role), workspace_role=self.workspace.get_user_role(member.user), role=ProjectPermissions.get_user_project_role(self, member.user), + fullname=member.user.profile.name(), ) def members_by_role(self, role: ProjectRole) -> List[int]: @@ -364,6 +365,7 @@ class ProjectMember: workspace_role: WorkspaceRole project_role: Optional[ProjectRole] role: ProjectRole + fullname: str @dataclass @@ -376,6 +378,7 @@ class ProjectAccessDetail: workspace_role: str project_role: Optional[ProjectRole] type: str + last_signed_in: Optional[datetime] class ProjectFilePath(db.Model): diff --git a/server/mergin/sync/private_api.yaml b/server/mergin/sync/private_api.yaml index 48a88933..4c799b3f 100644 --- a/server/mergin/sync/private_api.yaml +++ b/server/mergin/sync/private_api.yaml @@ -657,6 +657,12 @@ components: type: string format: date-time example: 2018-11-30T08:47:58.636074Z + last_signed_in: + description: Present only for type `member` + nullable: true + type: string + format: date-time + example: 2025-12-18T08:47:58Z ProjectAccessUpdated: type: object properties: diff --git a/server/mergin/sync/public_api_v2.yaml b/server/mergin/sync/public_api_v2.yaml index eb635d9a..6e868805 100644 --- a/server/mergin/sync/public_api_v2.yaml +++ b/server/mergin/sync/public_api_v2.yaml @@ -528,6 +528,10 @@ components: $ref: "#/components/schemas/ProjectRole" role: $ref: "#/components/schemas/Role" + fullname: + nullable: true + type: string + example: John Doe ProjectDetail: type: object required: diff --git a/server/mergin/sync/public_api_v2_controller.py b/server/mergin/sync/public_api_v2_controller.py index 9909854b..ffd0d6fd 100644 --- a/server/mergin/sync/public_api_v2_controller.py +++ b/server/mergin/sync/public_api_v2_controller.py @@ -114,6 +114,7 @@ def get_project_collaborators(id): project_role=project_role, workspace_role=workspace_role, role=ProjectPermissions.get_user_project_role(project, user), + fullname=user.profile.name(), ) ) diff --git a/server/mergin/sync/schemas.py b/server/mergin/sync/schemas.py index 8d1df050..5ad41a9a 100644 --- a/server/mergin/sync/schemas.py +++ b/server/mergin/sync/schemas.py @@ -364,6 +364,7 @@ class ProjectAccessDetailSchema(Schema): workspace_role = fields.Enum(enum=WorkspaceRole, by_value=True) type = fields.String() invitation = fields.Nested(ProjectInvitationAccessSchema()) + last_signed_in = DateTimeWithZ() class ProjectVersionListSchema(ma.SQLAlchemyAutoSchema): @@ -405,6 +406,7 @@ class ProjectMemberSchema(Schema): project_role = fields.Enum(enum=ProjectRole, by_value=True) workspace_role = fields.Enum(enum=WorkspaceRole, by_value=True) role = fields.Enum(enum=ProjectRole, by_value=True) + fullname = fields.String() class UploadChunkSchema(Schema): diff --git a/server/mergin/sync/workspace.py b/server/mergin/sync/workspace.py index 76245ef3..424194d0 100644 --- a/server/mergin/sync/workspace.py +++ b/server/mergin/sync/workspace.py @@ -341,6 +341,7 @@ def project_access(self, project: Project) -> List[ProjectAccessDetail]: role=project_permission and project_permission.value, project_role=project_role.value if project_role else None, type="member", + last_signed_in=dm.last_signed_in, ) result.append(member) if global_role: @@ -355,6 +356,7 @@ def project_access(self, project: Project) -> List[ProjectAccessDetail]: role=global_role, project_role=None, type="member", + last_signed_in=gm.last_signed_in, ) result.append(member) return result diff --git a/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue b/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue index cb283c78..d3b537ff 100644 --- a/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue +++ b/web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue @@ -13,7 +13,7 @@
-
+ {{ fullName ? `${fullName} (${username})` : username }} +
+ ++ {{ email }} + (me) +
+