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
9 changes: 6 additions & 3 deletions server/mergin/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
user_account_closed,
)
from .bearer import encode_token
from .models import User, LoginHistory
from .models import User, LoginHistory, UserProfile
from .schemas import UserSchema, UserSearchSchema, UserProfileSchema, UserInfoSchema
from .forms import (
LoginForm,
Expand Down Expand Up @@ -449,13 +449,16 @@ def get_paginated_users(

:rtype: Dict[str: List[User], str: Integer]
"""
users = User.query.filter(
users = User.query.join(UserProfile).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}%")
User.username.ilike(f"%{like}%")
| User.email.ilike(f"%{like}%")
| UserProfile.first_name.ilike(f"%{like}%")
| UserProfile.last_name.ilike(f"%{like}%")
)

if descending and order_by:
Expand Down
2 changes: 1 addition & 1 deletion web-app/packages/admin-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<title>Mergin Maps</title>
<title>Mergin Maps Admin Panel</title>
</head>
<body>
<noscript>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const sidebarItems = computed<SideBarItemModel[]>(() => [

<template>
<side-bar-template :sidebarItems="sidebarItems">
<template #subtitle>Administration</template>
<template #subtitle>Admin panel</template>
<template #footer>
<sidebar-footer />
</template>
Expand Down
5 changes: 4 additions & 1 deletion web-app/packages/admin-lib/src/modules/admin/adminApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ApiRequestSuccessInfo,
errorUtils,
LoginData,
PaginatedUsersParams,
UserProfileResponse,
UserResponse /*, getDefaultRetryOptions */
} from '@mergin/lib'
Expand All @@ -27,7 +28,9 @@ export const AdminApi = {
return AdminModule.httpService.post('/app/admin/login', data)
},

async fetchUsers(params: UsersParams): Promise<AxiosResponse<UsersResponse>> {
async fetchUsers(
params: PaginatedUsersParams
): Promise<AxiosResponse<UsersResponse>> {
return AdminModule.httpService.get(`/app/admin/users`, { params })
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<span class="p-input-icon-left w-full">
<i class="ti ti-search paragraph-p3"></i>
<PInputText
placeholder="Search members"
placeholder="Search accounts"
data-cy="search-members-field"
v-model="searchByName"
class="w-full"
Expand Down Expand Up @@ -141,9 +141,9 @@ export default defineComponent({
},
searchByName: '',
headers: [
{ field: 'username', header: 'Name', sortable: true },
{ field: 'username', header: 'Username', sortable: true },
{ field: 'email', header: 'Email', sortable: true },
{ field: 'profile.name', header: 'Username' },
{ field: 'profile.name', header: 'Full name' },
{ field: 'active', header: 'Active' }
] as TableDataHeader[]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
:sortable="header.sortable"
>
<template #body="slotProps">
<router-link
class="title-t4"
:to="{
name: `adminWorkspace`,
params: { id: slotProps.data.workspace_id }
}"
>
{{ slotProps.data.workspace }}
</router-link>
{{ slotProps.data.workspace }}
</template>
</PColumn>

Expand All @@ -74,16 +66,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
:sortable="header.sortable"
>
<template #body="slotProps">
<templte v-if="slotProps.data.removed_at">{{
slotProps.data.name
}}</templte>
<router-link
v-else
:to="{
name: 'project',
params: {
namespace: slotProps.data.workspace,
projectName: slotProps.data.name
}
}"
class="font-semibold"
>
<strong>{{ slotProps.data.name }}</strong>
{{ slotProps.data.name }}
</router-link>
</template>
</PColumn>
Expand Down Expand Up @@ -119,7 +116,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
:sortable="header.sortable"
>
<template #body="slotProps">
<span :title="$filters.datetime(slotProps.data.removed_at)">
<span
:title="`Scheduled for removal at ${$filters.datetime(
slotProps.data.removed_at
)}`"
>
{{ $filters.timediff(slotProps.data.removed_at) }}
</span>
</template>
Expand All @@ -131,12 +132,19 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
class="justify-center px-0"
v-if="slotProps.data.removed_at"
>
<div style="text-align: end">
<div class="flex align-items-center gap-1">
<PButton
label="Restore"
severity="secondary"
size="small"
@click="confirmRestore(slotProps.data)"
/>
<PButton
label="Delete"
severity="danger"
size="small"
@click="confirmDelete(slotProps.data)"
/>
</div>
</div>
</template>
Expand Down Expand Up @@ -228,7 +236,7 @@ export default defineComponent({
{ header: 'Name', field: 'name', sortable: true },
{ header: 'Last Update', field: 'updated', sortable: true },
{ header: 'Size', field: 'disk_usage', sortable: true },
{ header: 'Removed', field: 'removed_at', sortable: true },
{ header: 'Scheduled removal at', field: 'removed_at', sortable: true },
{ header: 'Removed by', field: 'removed_by', sortable: true },
{ header: '', field: 'buttons', sortable: false }
]
Expand All @@ -241,7 +249,11 @@ export default defineComponent({
methods: {
...mapActions(useDialogStore, { showDialog: 'show' }),
...mapActions(useNotificationStore, ['error', 'show']),
...mapActions(useAdminStore, ['getProjects', 'restoreProject']),
...mapActions(useAdminStore, [
'getProjects',
'restoreProject',
'deleteProject'
]),

paginating(options) {
this.options = options
Expand Down Expand Up @@ -302,6 +314,31 @@ export default defineComponent({
})
},

confirmDelete(item) {
const props: ConfirmDialogProps = {
text: `Are you sure you want to permanently delete this project?`,
description: `Deleting this project will remove it
and all its data. This action cannot be undone. Type in project name to confirm:`,
hint: item.name,
confirmText: 'Delete permanently',
confirmField: {
label: 'Project name',
expected: item.name
},
severity: 'danger'
}
const listeners = {
confirm: async () => {
await this.deleteProject({ projectId: item.id })
this.fetchProjects()
}
}
this.showDialog({
component: ConfirmDialog,
params: { props, listeners, dialog: { header: 'Delete project' } }
})
},

onRefresh() {
this.fetchProjects()
}
Expand Down
7 changes: 5 additions & 2 deletions web-app/packages/admin-lib/src/modules/admin/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
errorUtils,
htmlUtils,
LoginPayload,
PaginatedUsersParams,
SortingOptions,
useFormStore,
useInstanceStore,
Expand All @@ -14,6 +15,9 @@ import {
} from '@mergin/lib'
import { defineStore, getActivePinia } from 'pinia'
import Cookies from 'universal-cookie'

import { AdminRoutes } from './routes'

import { AdminApi } from '@/modules/admin/adminApi'
import {
LatestServerVersionResponse,
Expand All @@ -22,7 +26,6 @@ import {
UpdateUserPayload,
UsersResponse
} from '@/modules/admin/types'
import { AdminRoutes } from './routes'

export interface AdminState {
loading: boolean
Expand Down Expand Up @@ -110,7 +113,7 @@ export const useAdminStore = defineStore('adminModule', {
this.isServerConfigHidden = value
},

async fetchUsers(payload) {
async fetchUsers(payload: { params: PaginatedUsersParams }) {
const notificationStore = useNotificationStore()

this.setLoading(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,17 @@
</div>
</app-section>
</app-container>
<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">
<i v-if="profile?.receive_notifications" class="ti ti-check" />
<i v-else class="ti ti-x" />
<PInputSwitch
:model-value="profile?.receive_notifications"
disabled
/>
</div>
</template>
<template #adminAccess>
Expand All @@ -78,9 +80,15 @@
class="flex align-items-center flex-shrink-0"
data-cy="profile-notification"
>
<PInputSwitch
<PButton
:severity="user?.is_admin ? 'danger' : 'warning'"
:modelValue="user?.is_admin"
@change="switchAdminAccess"
@click="switchAdminAccess"
:label="
!user?.is_admin
? 'Grant admin access'
: 'Revoke admin access'
"
/>
</div>
</div>
Expand All @@ -89,7 +97,7 @@
<div class="flex-shrink-0">
<PButton
@click="changeStatusDialog"
:severity="user?.active ? 'danger' : 'secondary'"
:severity="user?.active ? 'warning' : 'secondary'"
:label="
user?.active ? 'Deactivate account' : 'Activate account'
"
Expand Down Expand Up @@ -121,7 +129,8 @@ import {
AppContainer,
ConfirmDialogProps,
AppSettings,
AppSettingsItemConfig
AppSettingsItemConfig,
useUserStore
} from '@mergin/lib'
import { computed, watch } from 'vue'
import { useRoute } from 'vue-router'
Expand All @@ -132,6 +141,7 @@ import { useAdminStore } from '@/modules/admin/store'
const route = useRoute()
const adminStore = useAdminStore()
const dialogStore = useDialogStore()
const userStore = useUserStore()

const settingsItems = computed<AppSettingsItemConfig[]>(() => [
{
Expand All @@ -144,7 +154,9 @@ const settingsItems = computed<AppSettingsItemConfig[]>(() => [
{
key: 'adminAccess',
title: 'Access to admin panel',
description: 'Enabling this option will provide access to the admin panel.'
description: user.value?.is_admin
? 'User has access to the admin panel.'
: 'User does not have access to the admin panel.'
},
{
key: 'accountActivation',
Expand Down Expand Up @@ -181,12 +193,18 @@ watch(
)

const changeStatusDialog = () => {
const props: ConfirmDialogProps = {
confirmText: 'Yes',
text: user.value.active
? 'Do you really want deactivate this account?'
: 'Do you really want activate this account?'
}
const props: ConfirmDialogProps = user.value.active
? {
confirmText: 'Deactivate',
severity: 'warning',
text: 'Do you really want deactivate this account?',
description:
'Deactivating this account will lead to a temporary ban from Mergin Maps usage.'
}
: {
text: 'Do you really want activate this account?',
confirmText: 'Activate'
}
const dialog = { header: 'User activation' }
const listeners = {
confirm: async () => {
Expand Down Expand Up @@ -233,12 +251,42 @@ const confirmDeleteUser = () => {
}

const switchAdminAccess = async () => {
await adminStore.updateUser({
username: user.value.username,
data: {
active: user.value.active,
is_admin: !user.value.is_admin
}
const props: ConfirmDialogProps = !user.value?.is_admin
? {
text: `Are you sure to grant access to admin panel to this user?`,
description: `This person will have full management access to all data on the server. They will see all users and projects and can update or remove them.`,
hint: user.value.username,
confirmText: 'Grant access',
confirmField: {
label: 'Username',
expected: user.value.username
},
severity: 'warning'
}
: {
text: `Are you sure you want to revoke access to admin panel to this user?`,
description: `This person will no longer have access to the admin panel.`,
hint: user.value.username,
confirmText: 'Revoke access',
confirmField: {
label: 'Username',
expected: user.value.username
},
severity: 'danger'
}
const listeners = {
confirm: async () =>
await adminStore.updateUser({
username: user.value.username,
data: {
active: user.value.active,
is_admin: !user.value.is_admin
}
})
}
dialogStore.show({
component: ConfirmDialog,
params: { props, listeners, dialog: { header: 'Admin access' } }
})
}
</script>
Expand Down
Loading