Skip to content
Open
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
5 changes: 5 additions & 0 deletions server/mergin/auth/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions server/mergin/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion server/mergin/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
3 changes: 2 additions & 1 deletion server/mergin/auth/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -70,6 +70,7 @@ class Meta:
"profile",
"scheduled_removal",
"registration_date",
"last_signed_in",
)
load_instance = True

Expand Down
2 changes: 2 additions & 0 deletions server/mergin/sync/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -364,6 +365,7 @@ class ProjectMember:
workspace_role: WorkspaceRole
project_role: Optional[ProjectRole]
role: ProjectRole
name: Optional[str]


@dataclass
Expand Down
6 changes: 6 additions & 0 deletions server/mergin/sync/private_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions server/mergin/sync/public_api_v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions server/mergin/sync/public_api_v2_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
)

Expand Down
1 change: 1 addition & 0 deletions server/mergin/sync/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
206 changes: 106 additions & 100 deletions web-app/packages/admin-lib/src/modules/admin/views/AccountDetailView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,116 +7,122 @@
</template>
</app-section>
</app-container>

<app-container>
<app-section class="p-4">
<div class="flex flex-column align-items-center row-gap-3 text-center">
<PAvatar
v-if="user"
:label="$filters.getAvatar(user?.email, user?.username)"
size="xlarge"
shape="circle"
:pt="{
root: {
class: 'font-semibold text-color-forest'
}
}"
/>
<h3 class="headline-h2" data-cy="profile-username">
{{ user?.username }}
</h3>
<p
class="m-0 paragraph-p6 overflow-wrap-anywhere"
data-cy="profile-email"
<template v-if="user">
<app-container>
<app-section class="p-4">
<div
class="flex flex-column align-items-center row-gap-3 text-center"
>
<i
v-if="!user?.verified_email"
v-tooltip.top="{
value: 'Email verification status'
<PAvatar
:label="$filters.getAvatar(user?.email, profile?.name)"
size="xlarge"
shape="circle"
:pt="{
root: {
class: 'font-semibold text-color-forest'
}
}"
class="ti ti-alert-circle-filled"
style="color: var(--grape-color)"
></i>
{{ user?.email }}
</p>
<dl class="profile-view-detail-list grid grid-nogutter paragraph-p5">
<div
class="col-6 flex flex-column align-items-start text-left flex-wrap"
/>
<h3 class="headline-h2" data-cy="profile-name">
{{
profile?.name
? `${profile.name} (${user?.username})`
: user.username
}}
</h3>
<p
class="m-0 paragraph-p6 overflow-wrap-anywhere"
data-cy="profile-email"
>
<dt class="paragraph-p6 opacity-80 mb-2">Full name</dt>
<dd class="font-semibold" data-cy="profile-name">
{{ profile?.name || '-' }}
</dd>
</div>
<div class="col-6 flex flex-column align-items-end">
<dt class="paragraph-p6 opacity-80 mb-2">Registered</dt>
<dd class="font-semibold" data-cy="profile-registered">
{{ $filters.date(user?.registration_date) }}
</dd>
</div>
</dl>
</div>
</app-section>
</app-container>
<app-container v-if="userStore.loggedUser?.id !== user?.id">
<app-section>
<template #title>Advanced</template>

<app-settings :items="settingsItems">
<template #notifications>
<div class="flex-shrink-0 paragraph-p1">
<PInputSwitch
:model-value="profile?.receive_notifications"
disabled
/>
</div>
</template>
<template #adminAccess>
<div class="flex-shrink-0 paragraph-p1">
<i
v-if="!user?.verified_email"
v-tooltip.top="{
value: 'Email verification status'
}"
class="ti ti-alert-circle-filled"
style="color: var(--grape-color)"
></i>
{{ user?.email }}
</p>
<dl class="profile-view-detail-list grid grid-nogutter paragraph-p5">
<div
class="flex align-items-center flex-shrink-0"
data-cy="profile-notification"
class="col-6 flex flex-column align-items-start text-left flex-wrap"
>
<dt class="paragraph-p6 opacity-80 mb-2">Last signed in</dt>
<dd class="font-semibold" data-cy="profile-last-signed-in">
{{ $filters.date(user.last_signed_in) || '-' }}
</dd>
</div>
<div class="col-6 flex flex-column align-items-end">
<dt class="paragraph-p6 opacity-80 mb-2">Registered</dt>
<dd class="font-semibold" data-cy="profile-registered">
{{ $filters.date(user?.registration_date) }}
</dd>
</div>
</dl>
</div>
</app-section>
</app-container>
<app-container v-if="userStore.loggedUser?.id !== user?.id">
<app-section>
<template #title>Advanced</template>

<app-settings :items="settingsItems">
<template #notifications>
<div class="flex-shrink-0 paragraph-p1">
<PInputSwitch
:model-value="profile?.receive_notifications"
disabled
/>
</div>
</template>
<template #adminAccess>
<div class="flex-shrink-0 paragraph-p1">
<div
class="flex align-items-center flex-shrink-0"
data-cy="profile-notification"
>
<PButton
:severity="user?.is_admin ? 'danger' : 'warning'"
:disabled="
!instanceStore.configData?.enable_superadmin_assignment
"
@click="switchAdminAccess"
:label="
!user?.is_admin
? 'Grant admin access'
: 'Revoke admin access'
"
/>
</div>
</div>
</template>
<template #accountActivation>
<div class="flex-shrink-0">
<PButton
:severity="user?.is_admin ? 'danger' : 'warning'"
:disabled="
!instanceStore.configData?.enable_superadmin_assignment
"
@click="switchAdminAccess"
@click="changeStatusDialog"
:severity="user?.active ? 'warning' : 'secondary'"
:label="
!user?.is_admin
? 'Grant admin access'
: 'Revoke admin access'
user?.active ? 'Deactivate account' : 'Activate account'
"
class="w-auto mr-1"
/>
</div>
</div>
</template>
<template #accountActivation>
<div class="flex-shrink-0">
<PButton
@click="changeStatusDialog"
:severity="user?.active ? 'warning' : 'secondary'"
:label="
user?.active ? 'Deactivate account' : 'Activate account'
"
class="w-auto mr-1"
/>
</div>
</template>
<template #deleteAccount>
<div class="flex-shrink-0">
<PButton
@click="confirmDeleteUser"
severity="danger"
data-cy="profile-close-account-btn"
label="Delete account"
/>
</div>
</template>
</app-settings>
</app-section>
</app-container>
</template>
<template #deleteAccount>
<div class="flex-shrink-0">
<PButton
@click="confirmDeleteUser"
severity="danger"
data-cy="profile-close-account-btn"
label="Delete account"
/>
</div>
</template>
</app-settings>
</app-section>
</app-container>
</template>
</admin-layout>
</template>

Expand Down
37 changes: 37 additions & 0 deletions web-app/packages/lib/src/common/components/UserSummary.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<div class="flex align-items-center w-auto" style="gap: 10px">
<PAvatar
:label="$filters.getAvatar(email, name)"
size="large"
shape="circle"
:pt="{
root: {
class: 'text-color-forest font-semibold flex-shrink-0',
style: { borderRadius: '50%' }
}
}"
/>

<div class="flex flex-column justify-content-between">
<p class="title-t4 overflow-wrap-anywhere">
{{ name ? `${name} (${username})` : username }}
</p>

<p class="paragraph-p6 overflow-wrap-anywhere font-normal">
{{ email }}
<template v-if="isMe"> (me)</template>
</p>
</div>
</div>
</template>

<script setup lang="ts">
interface Props {
username: string
email: string
name?: string
isMe?: boolean
}

defineProps<Props>()
</script>
1 change: 1 addition & 0 deletions web-app/packages/lib/src/common/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Loading
Loading