Browse Source

Merge pull request #5 from Rox0z/Delete-Users

Delete Users com mensagens BD
pull/2430/head
Leonardo Albudane 3 months ago
committed by GitHub
parent
commit
2ed0b1974c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
5 changed files with 211 additions and 143 deletions
  1. 14
      backend/users/views.py
  2. 33
      frontend/components/common/ErrorDialog.vue
  3. 16
      frontend/components/utils/BaseCard.vue
  4. 208
      frontend/pages/users/_id.vue
  5. 83
      frontend/pages/users/index.vue

14
backend/users/views.py

@ -5,7 +5,7 @@ from rest_framework import filters, generics, status
from rest_framework.permissions import IsAdminUser, IsAuthenticated, AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from rest_framework.exceptions import PermissionDenied, ValidationError
from .serializers import UserSerializer, UserDetailSerializer
from projects.permissions import IsProjectAdmin
@ -17,6 +17,7 @@ class Me(APIView):
serializer = UserDetailSerializer(request.user, context={"request": request})
return Response(serializer.data)
class UserDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = User.objects.all()
serializer_class = UserDetailSerializer
@ -24,19 +25,27 @@ class UserDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticated & (IsProjectAdmin | IsAdminUser)]
filter_backends = (DjangoFilterBackend,)
filterset_fields = ("username",)
def get_queryset(self):
return self.queryset
def perform_update(self, serializer):
if serializer.validated_data.get("is_superuser") and not self.request.user.is_superuser:
raise PermissionDenied("You do not have permission to make this user a superuser.")
if serializer.validated_data.get("is_staff") and not self.request.user.is_superuser:
raise PermissionDenied("You do not have permission to make this user a staff member.")
return super().perform_update(serializer)
def perform_destroy(self, instance):
if instance == self.request.user:
raise PermissionDenied("You cannot delete your own account.")
if instance.is_superuser:
raise ValidationError(f"User '{instance.username}' is an administrator and cannot be deleted.")
if instance.is_staff:
raise ValidationError(f"User '{instance.username}' is a staff member and cannot be deleted.")
instance.delete()
class Users(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
@ -44,8 +53,7 @@ class Users(generics.ListAPIView):
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)
search_fields = ("username",)
ordering_fields = ("username", "email", "is_staff", "is_superuser")
ordering = ("username",) # Default ordering
# Direction can be controlled by using "-" prefix (e.g., "-username" for descending)
ordering = ("username",)
class UserCreation(generics.CreateAPIView):

33
frontend/components/common/ErrorDialog.vue

@ -0,0 +1,33 @@
<template>
<v-dialog :value="visible" max-width="420" @input="$emit('close')">
<v-card class="rounded-lg elevation-10">
<v-card-title class="white--text d-flex align-center" style="background-color: #f44336cc;">
<v-icon left class="mr-2">mdi-alert-circle</v-icon>
<span class="headline">Error</span>
</v-card-title>
<v-card-text class="pa-5 text-body-1 grey--text text--darken-3">
{{ message }}
</v-card-text>
<v-card-actions class="px-5 pb-4">
<v-spacer></v-spacer>
<v-btn text class="red--text" @click="$emit('close')">OK</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
export default {
name: 'ErrorDialog',
props: {
visible: {
type: Boolean,
required: true
},
message: {
type: String,
default: ''
}
}
}
</script>

16
frontend/components/utils/BaseCard.vue

@ -1,12 +1,20 @@
<template>
<v-card>
<v-card
max-width="500"
class="rounded-lg elevation-10 mx-auto"
>
<!-- Topo azul com título -->
<v-toolbar color="primary white--text" flat>
<v-toolbar-title>{{ title }}</v-toolbar-title>
</v-toolbar>
<v-card-text class="text--primary mt-3 pl-4">
<!-- Corpo do conteúdo -->
<v-card-text class="text--primary px-5 py-4">
<slot name="content" />
</v-card-text>
<v-card-actions>
<!-- Botões alinhados -->
<v-card-actions class="px-5 pb-4">
<v-spacer />
<v-btn
v-if="cancelText"
@ -23,6 +31,7 @@
:disabled="disabled"
class="text-none"
text
color="error"
data-test="delete-button"
@click="agree"
>
@ -32,6 +41,7 @@
</v-card>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({

208
frontend/pages/users/_id.vue

@ -2,13 +2,13 @@
<v-container>
<v-card>
<v-card-title class="headline">
<v-btn icon
@click.stop="goBack">
<v-icon>{{ icons.mdiArrowLeft }}</v-icon>
<v-btn icon @click.stop="goBack">
<v-icon>{{ icons.mdiArrowLeft }}</v-icon>
</v-btn>
<span v-if="user">User Settings</span>
<span v-else>Loading user profile...</span>
</v-card-title>
<v-card-text>
<v-alert v-if="errorMessage" type="error" dismissible>
{{ errorMessage }}
@ -16,117 +16,82 @@
<v-alert v-if="successMessage" type="success" dismissible>
{{ successMessage }}
</v-alert>
<div v-if="user">
<p>Username: {{ user.username }}
<v-chip
v-if="user.isStaff"
color="amber"
class="ml-2"
x-small
>
Staff
</v-chip>
<v-chip
v-if="user.isSuperUser"
color="orange"
class="ml-2"
x-small
>
Admin
</v-chip>
<v-chip
v-if="user.isActive"
color="blue"
class="ml-2"
x-small
>
Active
</v-chip>
<v-chip
v-else
color="red"
class="ml-2"
x-small
>
Inactive
</v-chip>
</p>
<p>
Email: {{ user.email }}
</p>
<p>Fist name: {{ user.firstName }}</p>
<p>Last name: {{ user.lastName }}</p>
<p>Joined at: {{ new Date(user.dateJoined).toLocaleString() }}</p>
<p>
Username: {{ user.username }}
<v-chip v-if="user.isStaff" color="amber" class="ml-2" x-small>Staff</v-chip>
<v-chip v-if="user.isSuperUser" color="orange" class="ml-2" x-small>Admin</v-chip>
<v-chip v-if="user.isActive" color="blue" class="ml-2" x-small>Active</v-chip>
<v-chip v-else color="red" class="ml-2" x-small>Inactive</v-chip>
</p>
<p>Email: {{ user.email }}</p>
<p>First name: {{ user.firstName }}</p>
<p>Last name: {{ user.lastName }}</p>
<p>Joined at: {{ new Date(user.dateJoined).toLocaleString() }}</p>
</div>
<v-divider class="mb-5"></v-divider>
<v-divider class="mb-5" />
<v-form v-if="user" ref="form" @submit.prevent="updateUser">
<h2 class="mb-5">Edit User:</h2>
<v-text-field
v-model="editedUser.username"
label="Username"
required
></v-text-field>
<v-switch
v-model="editedUser.isStaff"
color="amber"
label="Staff"
hint="User has staff privileges"
></v-switch>
<v-switch
v-model="editedUser.isSuperUser"
color="orange"
label="Administrator"
hint="User has administrative privileges"
></v-switch>
<h2 class="mb-5">Edit User:</h2>
<v-text-field v-model="editedUser.username" label="Username" required />
<v-switch v-model="editedUser.isStaff" color="amber" label="Staff" />
<v-switch v-model="editedUser.isSuperUser" color="orange" label="Administrator" />
<v-card-actions>
<v-btn
color="error"
@click="confirmDelete = true"
:disabled="loading"
>
Delete User
</v-btn>
<v-spacer></v-spacer>
<v-btn
color="primary"
class="mr-4"
type="submit"
:loading="loading"
:disabled="loading"
>
<v-btn color="error" @click="handleSingleDelete" :disabled="loading">Delete User</v-btn>
<v-spacer />
<v-btn color="primary" class="mr-4" type="submit" :loading="loading" :disabled="loading">
Update Profile
</v-btn>
</v-card-actions>
</v-form>
<v-progress-circular v-else indeterminate color="primary"></v-progress-circular>
<v-progress-circular v-else indeterminate color="primary" />
</v-card-text>
</v-card>
<v-dialog v-model="confirmDelete" max-width="400">
<v-card>
<v-card-title>Delete User</v-card-title>
<v-card-text>
<v-dialog v-model="confirmDelete" max-width="420">
<v-card class="rounded-lg elevation-10">
<!-- Cabeçalho azul com ícone -->
<v-card-title class="white--text d-flex align-center" style="background-color: #1976d2;">
<v-icon left class="mr-2">mdi-help-circle</v-icon>
<span class="headline">Delete User</span>
</v-card-title>
<v-card-text class="pa-5 text-body-1 grey--text text--darken-3">
Are you sure you want to delete this user? This action cannot be undone.
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-card-actions class="px-5 pb-4">
<v-spacer />
<v-btn text @click="confirmDelete = false">Cancel</v-btn>
<v-btn color="error" text @click="deleteUser" :loading="deleteLoading">Delete</v-btn>
<v-btn text class="red--text" @click="deleteUser" :loading="deleteLoading">Delete</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Modal de erro -->
<error-dialog :visible="errorDialog" :message="errorDialogMessage" @close="errorDialog = false" />
</v-container>
</template>
<script lang="ts">
import Vue from 'vue'
import { mdiArrowLeft } from '@mdi/js'
import ErrorDialog from '@/components/common/ErrorDialog.vue'
import { UserApplicationService } from '@/services/application/user/UserApplicationService'
import { APIUserRepository } from '@/repositories/user/apiUserRepository'
import { UserDetails } from '@/domain/models/user/user'
export default Vue.extend({
layout: 'projects',
components: {
ErrorDialog
},
layout: 'projects',
data() {
return {
user: null as UserDetails | null,
@ -138,6 +103,8 @@ export default Vue.extend({
loading: false,
deleteLoading: false,
confirmDelete: false,
errorDialog: false,
errorDialogMessage: '',
errorMessage: '',
successMessage: '',
icons: {
@ -145,11 +112,11 @@ export default Vue.extend({
}
}
},
async created() {
await this.fetchUser()
},
methods: {
async fetchUser() {
try {
@ -165,22 +132,21 @@ export default Vue.extend({
this.errorMessage = error.message
}
},
async updateUser() {
try {
this.loading = true
this.errorMessage = ''
this.successMessage = ''
const id = parseInt(this.$route.params.id)
const userService = new UserApplicationService(new APIUserRepository())
await userService.updateUser(id, {
username: this.editedUser.username,
is_staff: this.editedUser.isStaff,
is_superuser: this.editedUser.isSuperUser
})
this.successMessage = 'User profile updated successfully'
await this.fetchUser()
} catch (error) {
@ -189,28 +155,50 @@ export default Vue.extend({
this.loading = false
}
},
async deleteUser() {
try {
this.deleteLoading = true
this.errorMessage = ''
const id = parseInt(this.$route.params.id)
const userService = new UserApplicationService(new APIUserRepository())
await userService.deleteUser(id)
this.confirmDelete = false
this.$router.push('/users')
} catch (error) {
this.errorMessage = error.message
this.confirmDelete = false
} finally {
this.deleteLoading = false
handleSingleDelete() {
if (this.user?.isSuperUser || this.user?.isStaff) {
this.errorDialogMessage = `Cannot delete: ${this.user.username}. Admins and staff cannot be deleted.`
this.errorDialog = true
return
}
this.confirmDelete = true
},
async deleteUser() {
try {
this.deleteLoading = true
this.errorMessage = ''
const id = parseInt(this.$route.params.id)
const userService = new UserApplicationService(new APIUserRepository())
await userService.deleteUser(id)
this.confirmDelete = false
this.$router.push('/users')
} catch (error) {
if (
error.message.includes('Failed to fetch') || // fetch padrão
error.message.includes('Network Error') || // Axios/network
error.message.includes('ECONNREFUSED') || // Node.js/axios backend offline
error.message.includes('502') || // Bad Gateway (nginx etc.)
error.message.includes('504') || // Gateway Timeout
error.message.includes('Failed to delete user') // custom errors
) {
this.errorDialogMessage =
'Unable to delete user: database connection failed. Check if the server is up.'
} else {
this.errorDialogMessage = error.message
}
this.errorDialog = true
this.confirmDelete = false
} finally {
this.deleteLoading = false
}
},
goBack() {
this.$router.go(-1)
this.$router.go(-1)
}
}
})

83
frontend/pages/users/index.vue

@ -1,18 +1,31 @@
<template>
<v-card>
<!-- Título com botão "Delete" -->
<v-card-title v-if="isStaff">
<v-btn
class="text-capitalize ms-2"
:disabled="!canDelete"
outlined
@click.stop="dialogDelete = true"
@click.stop="beforeOpenDeleteDialog"
>
{{ $t('generic.delete') }}
</v-btn>
<v-dialog v-model="dialogDelete">
<form-delete :selected="selected" @cancel="dialogDelete = false" @remove="remove" />
</v-dialog>
</v-card-title>
<!-- Modal de confirmação de exclusão -->
<v-dialog v-model="dialogDelete" max-width="500" scrollable>
<form-delete :selected="selected" @cancel="dialogDelete = false" @remove="remove" />
</v-dialog>
<!-- Modal de erro -->
<error-dialog
:visible="errorDialog"
:message="errorMessage"
@close="errorDialog = false"
/>
<!-- Lista de usuários -->
<user-list
v-model="selected"
:items="users"
@ -29,20 +42,23 @@ import Vue from 'vue'
import { mapGetters } from 'vuex'
import UserList from '@/components/user/UserList.vue'
import FormDelete from '~/components/user/FormDelete.vue'
import ErrorDialog from '@/components/common/ErrorDialog.vue'
import { User } from '~/domain/models/user/user'
export default Vue.extend({
components: {
UserList,
FormDelete
FormDelete,
ErrorDialog
},
layout: 'projects',
middleware: ['check-auth', 'auth'],
data() {
return {
dialogDelete: false,
errorDialog: false,
errorMessage: '',
users: [] as User[],
selected: [] as User[],
isLoading: false,
@ -58,10 +74,8 @@ export default Vue.extend({
this.users = list.results
this.total = list.count
} catch (e) {
this.$store.dispatch('notification/setNotification', {
color: 'error',
text: 'Failed to load users'
})
this.errorMessage = 'Failed to load users'
this.errorDialog = true
} finally {
this.isLoading = false
}
@ -92,26 +106,41 @@ export default Vue.extend({
return query
},
beforeOpenDeleteDialog() {
const blockedUsers = this.selected.filter(user => user.isSuperUser || user.isStaff)
if (blockedUsers.length > 0) {
const names = blockedUsers.map(u => u.username).join(', ')
this.errorMessage = `Cannot delete: ${names}. Admins and staff cannot be deleted.`
this.errorDialog = true
return
}
this.dialogDelete = true
},
async remove() {
try {
for (const user of this.selected) {
try {
for (const user of this.selected) {
await this.$services.user.deleteUser(user.id)
}
this.$store.dispatch('notification/setNotification', {
color: 'success',
text: 'Users deleted successfully'
})
this.$fetch()
} catch (e) {
this.$store.dispatch('notification/setNotification', {
color: 'error',
text: 'Failed to delete users'
})
} finally {
this.dialogDelete = false
this.selected = []
await this.$services.user.deleteUser(user.id)
} catch (error) {
throw new Error('network-failure') // personaliza o erro para tratar abaixo
}
},
}
this.dialogDelete = false
this.selected = []
this.$fetch()
} catch (e) {
this.dialogDelete = false
const message = String(e.message || e)
if (message === 'network-failure') {
this.errorMessage =
'Unable to delete user: database connection failed. Check if the server is up.'
} else {
this.errorMessage = message
}
this.errorDialog = true
}
},
updateQuery(query: object) {
this.$router.push(query)

Loading…
Cancel
Save