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/controller.py b/server/mergin/auth/controller.py index 3e00ce16..0a862384 100644 --- a/server/mergin/auth/controller.py +++ b/server/mergin/auth/controller.py @@ -424,6 +424,9 @@ def register_user(): # pylint: disable=W0613,W0612 @auth_required(permissions=["admin"]) def get_user(username): user = User.query.filter(User.username == username).first_or_404() + if not user.last_signed_in: + last_signed_in = LoginHistory.get_users_last_signed_in([user.id]) + user.last_signed_in = last_signed_in.get(user.id) data = UserSchema().dump(user) return data, 200 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..62e4c8b1 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), + name=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 + name: Optional[str] @dataclass 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..fcce84d2 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" + name: + 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..b6011746 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), + name=user.profile.name(), ) ) diff --git a/server/mergin/sync/schemas.py b/server/mergin/sync/schemas.py index 8d1df050..4eecca0c 100644 --- a/server/mergin/sync/schemas.py +++ b/server/mergin/sync/schemas.py @@ -405,6 +405,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) + name = fields.String() class UploadChunkSchema(Schema): 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..9096e9bb 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 @@ -7,116 +7,122 @@ - - - -
- -

- {{ user?.username }} -

-

+ + +

- - {{ user?.email }} -

-
-
+

+ {{ + profile?.name + ? `${profile.name} (${user?.username})` + : user.username + }} +

+

-

Full name
-
- {{ profile?.name || '-' }} -
-
-
-
Registered
-
- {{ $filters.date(user?.registration_date) }} -
-
-
-
- - - - - - - - - + + + + + diff --git a/web-app/packages/lib/src/common/components/UserSummary.vue b/web-app/packages/lib/src/common/components/UserSummary.vue new file mode 100644 index 00000000..934b95e1 --- /dev/null +++ b/web-app/packages/lib/src/common/components/UserSummary.vue @@ -0,0 +1,37 @@ + + + diff --git a/web-app/packages/lib/src/common/components/index.ts b/web-app/packages/lib/src/common/components/index.ts index 790534e6..59677e76 100644 --- a/web-app/packages/lib/src/common/components/index.ts +++ b/web-app/packages/lib/src/common/components/index.ts @@ -16,6 +16,7 @@ export { default as FullStorageWarningTemplate } from './FullStorageWarningTempl export { default as TipMessage } from './TipMessage.vue' export { default as AppOnboardingPage } from './AppOnboardingPage.vue' export { default as UsageCard } from './UsageCard.vue' +export { default as UserSummary } from './UserSummary.vue' export * from './types' export * from './data-view' export * from './app-settings' diff --git a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue index e7e01062..ef994b92 100644 --- a/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue +++ b/web-app/packages/lib/src/modules/project/components/ProjectMembersTable.vue @@ -38,33 +38,20 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial /> -