Skip to content

Commit 0b99238

Browse files
authored
Merge pull request #551 from MerginMaps/integrate_last_sign_in
Integrate last sign in
2 parents b52d8c8 + 1d54e3e commit 0b99238

File tree

15 files changed

+184
-131
lines changed

15 files changed

+184
-131
lines changed

server/mergin/auth/api.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,11 @@ components:
753753
type: string
754754
format: date-time
755755
example: 2023-07-30T08:47:58Z
756+
last_signed_in:
757+
nullable: true
758+
type: string
759+
format: date-time
760+
example: 2025-12-18T08:47:58Z
756761
profile:
757762
$ref: "#/components/schemas/UserProfile"
758763
PaginatedUsers:

server/mergin/auth/controller.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,9 @@ def register_user(): # pylint: disable=W0613,W0612
424424
@auth_required(permissions=["admin"])
425425
def get_user(username):
426426
user = User.query.filter(User.username == username).first_or_404()
427+
if not user.last_signed_in:
428+
last_signed_in = LoginHistory.get_users_last_signed_in([user.id])
429+
user.last_signed_in = last_signed_in.get(user.id)
427430
data = UserSchema().dump(user)
428431
return data, 200
429432

server/mergin/auth/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ class UserProfile(db.Model):
268268
),
269269
)
270270

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

274274

server/mergin/auth/schemas.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
class UserProfileSchema(ma.SQLAlchemyAutoSchema):
1313
name = ma.Function(
14-
lambda obj: f'{obj.first_name if obj.first_name else ""} {obj.last_name if obj.last_name else ""}'.strip(),
14+
lambda obj: obj.name(),
1515
dump_only=True,
1616
)
1717
storage = fields.Method("get_storage", dump_only=True)
@@ -70,6 +70,7 @@ class Meta:
7070
"profile",
7171
"scheduled_removal",
7272
"registration_date",
73+
"last_signed_in",
7374
)
7475
load_instance = True
7576

server/mergin/sync/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ def get_member(self, user_id: int) -> Optional[ProjectMember]:
304304
project_role=ProjectRole(member.role),
305305
workspace_role=self.workspace.get_user_role(member.user),
306306
role=ProjectPermissions.get_user_project_role(self, member.user),
307+
name=member.user.profile.name(),
307308
)
308309

309310
def members_by_role(self, role: ProjectRole) -> List[int]:
@@ -364,6 +365,7 @@ class ProjectMember:
364365
workspace_role: WorkspaceRole
365366
project_role: Optional[ProjectRole]
366367
role: ProjectRole
368+
name: Optional[str]
367369

368370

369371
@dataclass

server/mergin/sync/private_api.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,12 @@ components:
657657
type: string
658658
format: date-time
659659
example: 2018-11-30T08:47:58.636074Z
660+
last_signed_in:
661+
description: Present only for type `member`
662+
nullable: true
663+
type: string
664+
format: date-time
665+
example: 2025-12-18T08:47:58Z
660666
ProjectAccessUpdated:
661667
type: object
662668
properties:

server/mergin/sync/public_api_v2.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,10 @@ components:
528528
$ref: "#/components/schemas/ProjectRole"
529529
role:
530530
$ref: "#/components/schemas/Role"
531+
name:
532+
nullable: true
533+
type: string
534+
example: John Doe
531535
ProjectDetail:
532536
type: object
533537
required:

server/mergin/sync/public_api_v2_controller.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def get_project_collaborators(id):
114114
project_role=project_role,
115115
workspace_role=workspace_role,
116116
role=ProjectPermissions.get_user_project_role(project, user),
117+
name=user.profile.name(),
117118
)
118119
)
119120

server/mergin/sync/schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ class ProjectMemberSchema(Schema):
405405
project_role = fields.Enum(enum=ProjectRole, by_value=True)
406406
workspace_role = fields.Enum(enum=WorkspaceRole, by_value=True)
407407
role = fields.Enum(enum=ProjectRole, by_value=True)
408+
name = fields.String()
408409

409410

410411
class UploadChunkSchema(Schema):

web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue

Lines changed: 106 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -7,116 +7,122 @@
77
</template>
88
</app-section>
99
</app-container>
10-
11-
<app-container>
12-
<app-section class="p-4">
13-
<div class="flex flex-column align-items-center row-gap-3 text-center">
14-
<PAvatar
15-
v-if="user"
16-
:label="$filters.getAvatar(user?.email, user?.username)"
17-
size="xlarge"
18-
shape="circle"
19-
:pt="{
20-
root: {
21-
class: 'font-semibold text-color-forest'
22-
}
23-
}"
24-
/>
25-
<h3 class="headline-h2" data-cy="profile-username">
26-
{{ user?.username }}
27-
</h3>
28-
<p
29-
class="m-0 paragraph-p6 overflow-wrap-anywhere"
30-
data-cy="profile-email"
10+
<template v-if="user">
11+
<app-container>
12+
<app-section class="p-4">
13+
<div
14+
class="flex flex-column align-items-center row-gap-3 text-center"
3115
>
32-
<i
33-
v-if="!user?.verified_email"
34-
v-tooltip.top="{
35-
value: 'Email verification status'
16+
<PAvatar
17+
:label="$filters.getAvatar(user?.email, profile?.name)"
18+
size="xlarge"
19+
shape="circle"
20+
:pt="{
21+
root: {
22+
class: 'font-semibold text-color-forest'
23+
}
3624
}"
37-
class="ti ti-alert-circle-filled"
38-
style="color: var(--grape-color)"
39-
></i>
40-
{{ user?.email }}
41-
</p>
42-
<dl class="profile-view-detail-list grid grid-nogutter paragraph-p5">
43-
<div
44-
class="col-6 flex flex-column align-items-start text-left flex-wrap"
25+
/>
26+
<h3 class="headline-h2" data-cy="profile-name">
27+
{{
28+
profile?.name
29+
? `${profile.name} (${user?.username})`
30+
: user.username
31+
}}
32+
</h3>
33+
<p
34+
class="m-0 paragraph-p6 overflow-wrap-anywhere"
35+
data-cy="profile-email"
4536
>
46-
<dt class="paragraph-p6 opacity-80 mb-2">Full name</dt>
47-
<dd class="font-semibold" data-cy="profile-name">
48-
{{ profile?.name || '-' }}
49-
</dd>
50-
</div>
51-
<div class="col-6 flex flex-column align-items-end">
52-
<dt class="paragraph-p6 opacity-80 mb-2">Registered</dt>
53-
<dd class="font-semibold" data-cy="profile-registered">
54-
{{ $filters.date(user?.registration_date) }}
55-
</dd>
56-
</div>
57-
</dl>
58-
</div>
59-
</app-section>
60-
</app-container>
61-
<app-container v-if="userStore.loggedUser?.id !== user?.id">
62-
<app-section>
63-
<template #title>Advanced</template>
64-
65-
<app-settings :items="settingsItems">
66-
<template #notifications>
67-
<div class="flex-shrink-0 paragraph-p1">
68-
<PInputSwitch
69-
:model-value="profile?.receive_notifications"
70-
disabled
71-
/>
72-
</div>
73-
</template>
74-
<template #adminAccess>
75-
<div class="flex-shrink-0 paragraph-p1">
37+
<i
38+
v-if="!user?.verified_email"
39+
v-tooltip.top="{
40+
value: 'Email verification status'
41+
}"
42+
class="ti ti-alert-circle-filled"
43+
style="color: var(--grape-color)"
44+
></i>
45+
{{ user?.email }}
46+
</p>
47+
<dl class="profile-view-detail-list grid grid-nogutter paragraph-p5">
7648
<div
77-
class="flex align-items-center flex-shrink-0"
78-
data-cy="profile-notification"
49+
class="col-6 flex flex-column align-items-start text-left flex-wrap"
7950
>
51+
<dt class="paragraph-p6 opacity-80 mb-2">Last signed in</dt>
52+
<dd class="font-semibold" data-cy="profile-last-signed-in">
53+
{{ $filters.date(user.last_signed_in) || '-' }}
54+
</dd>
55+
</div>
56+
<div class="col-6 flex flex-column align-items-end">
57+
<dt class="paragraph-p6 opacity-80 mb-2">Registered</dt>
58+
<dd class="font-semibold" data-cy="profile-registered">
59+
{{ $filters.date(user?.registration_date) }}
60+
</dd>
61+
</div>
62+
</dl>
63+
</div>
64+
</app-section>
65+
</app-container>
66+
<app-container v-if="userStore.loggedUser?.id !== user?.id">
67+
<app-section>
68+
<template #title>Advanced</template>
69+
70+
<app-settings :items="settingsItems">
71+
<template #notifications>
72+
<div class="flex-shrink-0 paragraph-p1">
73+
<PInputSwitch
74+
:model-value="profile?.receive_notifications"
75+
disabled
76+
/>
77+
</div>
78+
</template>
79+
<template #adminAccess>
80+
<div class="flex-shrink-0 paragraph-p1">
81+
<div
82+
class="flex align-items-center flex-shrink-0"
83+
data-cy="profile-notification"
84+
>
85+
<PButton
86+
:severity="user?.is_admin ? 'danger' : 'warning'"
87+
:disabled="
88+
!instanceStore.configData?.enable_superadmin_assignment
89+
"
90+
@click="switchAdminAccess"
91+
:label="
92+
!user?.is_admin
93+
? 'Grant admin access'
94+
: 'Revoke admin access'
95+
"
96+
/>
97+
</div>
98+
</div>
99+
</template>
100+
<template #accountActivation>
101+
<div class="flex-shrink-0">
80102
<PButton
81-
:severity="user?.is_admin ? 'danger' : 'warning'"
82-
:disabled="
83-
!instanceStore.configData?.enable_superadmin_assignment
84-
"
85-
@click="switchAdminAccess"
103+
@click="changeStatusDialog"
104+
:severity="user?.active ? 'warning' : 'secondary'"
86105
:label="
87-
!user?.is_admin
88-
? 'Grant admin access'
89-
: 'Revoke admin access'
106+
user?.active ? 'Deactivate account' : 'Activate account'
90107
"
108+
class="w-auto mr-1"
91109
/>
92110
</div>
93-
</div>
94-
</template>
95-
<template #accountActivation>
96-
<div class="flex-shrink-0">
97-
<PButton
98-
@click="changeStatusDialog"
99-
:severity="user?.active ? 'warning' : 'secondary'"
100-
:label="
101-
user?.active ? 'Deactivate account' : 'Activate account'
102-
"
103-
class="w-auto mr-1"
104-
/>
105-
</div>
106-
</template>
107-
<template #deleteAccount>
108-
<div class="flex-shrink-0">
109-
<PButton
110-
@click="confirmDeleteUser"
111-
severity="danger"
112-
data-cy="profile-close-account-btn"
113-
label="Delete account"
114-
/>
115-
</div>
116-
</template>
117-
</app-settings>
118-
</app-section>
119-
</app-container>
111+
</template>
112+
<template #deleteAccount>
113+
<div class="flex-shrink-0">
114+
<PButton
115+
@click="confirmDeleteUser"
116+
severity="danger"
117+
data-cy="profile-close-account-btn"
118+
label="Delete account"
119+
/>
120+
</div>
121+
</template>
122+
</app-settings>
123+
</app-section>
124+
</app-container>
125+
</template>
120126
</admin-layout>
121127
</template>
122128

0 commit comments

Comments
 (0)