Browse Source

melhorias no criar utilizador

pull/2432/head
laurarodrigues3 3 months ago
parent
commit
680fda8b7d
14 changed files with 253 additions and 18 deletions
  1. 0
      backend/3.9
  2. 3
      backend/api/urls.py
  3. 34
      backend/api/views.py
  4. 8
      backend/users/serializers.py
  5. 66
      frontend/components/auth/FormRegister.vue
  6. 2
      frontend/components/user/UserList.vue
  7. 6
      frontend/domain/models/user/user.ts
  8. 2
      frontend/i18n/en/rules.js
  9. 49
      frontend/mixins/databaseHealthMixin.js
  10. 11
      frontend/repositories/auth/apiAuthRepository.ts
  11. 7
      frontend/repositories/user/apiUserRepository.ts
  12. 26
      frontend/services/database.service.js
  13. 53
      frontend/store/auth.js
  14. 4
      yarn.lock

0
backend/3.9

3
backend/api/urls.py

@ -1,7 +1,8 @@
from django.urls import path
from .views import TaskStatus
from .views import TaskStatus, DatabaseHealthCheck
urlpatterns = [
path(route="tasks/status/<task_id>", view=TaskStatus.as_view(), name="task_status"),
path(route="database/health", view=DatabaseHealthCheck.as_view(), name="database_health"),
]

34
backend/api/views.py

@ -2,6 +2,11 @@ from celery.result import AsyncResult
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from django.http import JsonResponse
from django.db import connection
from rest_framework import status, permissions
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
class TaskStatus(APIView):
@ -19,3 +24,32 @@ class TaskStatus(APIView):
"error": {"text": str(task.result)} if error else None,
}
)
@method_decorator(csrf_exempt, name='dispatch')
class DatabaseHealthCheck(APIView):
"""
API endpoint para verificar se a base de dados está disponível
"""
permission_classes = [permissions.AllowAny]
def get(self, request):
try:
# Tenta fazer uma query simples para testar a conexão
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
return JsonResponse({
'status': 'healthy',
'database': 'connected',
'message': 'Base de dados disponível'
}, status=200)
except Exception as e:
return JsonResponse({
'status': 'unhealthy',
'database': 'disconnected',
'message': 'Base de dados não disponível',
'error': str(e)
}, status=503)

8
backend/users/serializers.py

@ -10,7 +10,7 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("id", "username", "email", "is_staff", "is_superuser", "is_active", "groups", "groups_details")
fields = ("id", "username", "first_name", "last_name", "email", "is_staff", "is_superuser", "is_active", "groups", "groups_details")
def get_groups_details(self, obj):
groups_dict = {}
@ -47,10 +47,12 @@ class RegisterSerializer(serializers.ModelSerializer):
email = serializers.EmailField(required=True)
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
password_confirm = serializers.CharField(write_only=True, required=True)
first_name = serializers.CharField(required=True, max_length=150)
last_name = serializers.CharField(required=True, max_length=150)
class Meta:
model = User
fields = ('username', 'email', 'password', 'password_confirm')
fields = ('username', 'email', 'first_name', 'last_name', 'password', 'password_confirm')
def validate(self, attrs):
if attrs['password'] != attrs['password_confirm']:
@ -69,6 +71,8 @@ class RegisterSerializer(serializers.ModelSerializer):
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
first_name=validated_data['first_name'],
last_name=validated_data['last_name'],
password=validated_data['password']
)

66
frontend/components/auth/FormRegister.vue

@ -1,15 +1,23 @@
<template>
<base-card
:disabled="!valid"
:disabled="!valid || !isDatabaseHealthy"
:title="$t('user.register')"
:agree-text="$t('user.register')"
:cancel-text="$t('generic.cancel')"
@agree="tryRegister"
@cancel="cancelRegister"
>
<template #content>
<v-form v-model="valid">
<v-form v-model="valid" :disabled="!isDatabaseHealthy">
<v-alert v-show="showError" v-model="showError" type="error" dismissible>
{{ errorMessage }}
</v-alert>
<v-alert v-show="showSuccess" v-model="showSuccess" type="success" dismissible>
{{ successMessage }}
</v-alert>
<v-alert v-show="!isDatabaseHealthy" type="error" class="mb-4">
{{ databaseMessage }}
</v-alert>
<v-text-field
v-model="username"
:rules="userNameRules($t('rules.userNameRules'))"
@ -19,8 +27,25 @@
type="text"
autofocus
/>
<v-text-field
v-model="firstName"
:rules="firstNameRules"
label="Nome"
name="firstName"
:prepend-icon="mdiAccount"
type="text"
/>
<v-text-field
v-model="lastName"
:rules="lastNameRules"
label="Apelido"
name="lastName"
:prepend-icon="mdiAccount"
type="text"
/>
<v-text-field
v-model="email"
:rules="emailRules($t('rules.emailRules'))"
:label="$t('user.email')"
name="email"
:prepend-icon="mdiEmail"
@ -51,36 +76,46 @@
<script lang="ts">
import Vue from 'vue'
import { mdiAccount, mdiLock, mdiEmail } from '@mdi/js'
import { mdiAccount, mdiLock, mdiEmail, mdiDatabaseAlert } from '@mdi/js'
import { userNameRules, passwordRules, emailRules } from '@/rules/index'
import BaseCard from '@/components/utils/BaseCard.vue'
import { databaseHealthMixin } from '@/mixins/databaseHealthMixin'
export default Vue.extend({
components: {
BaseCard
},
mixins: [databaseHealthMixin],
props: {
register: {
type: Function,
default: () => Promise
default: () => () => Promise.resolve()
}
},
data() {
return {
valid: false,
username: '',
firstName: '',
lastName: '',
email: '',
password: '',
passwordConfirm: '',
userNameRules,
passwordRules,
emailRules,
firstNameRules: [(v: string) => !!v || 'Nome é obrigatório'],
lastNameRules: [(v: string) => !!v || 'Apelido é obrigatório'],
showError: false,
errorMessage: '',
showSuccess: false,
successMessage: '',
mdiAccount,
mdiLock,
mdiEmail
mdiEmail,
mdiDatabaseAlert
}
},
@ -98,15 +133,32 @@ export default Vue.extend({
try {
await this.register({
username: this.username,
firstName: this.firstName,
lastName: this.lastName,
email: this.email,
password: this.password,
passwordConfirm: this.passwordConfirm
})
this.$router.push(this.localePath('/users'))
// Mostrar mensagem de sucesso
this.showError = false
this.showSuccess = true
this.successMessage = `Utilizador "${this.username}" criado com sucesso!`
// Redirecionar após 3 segundos
setTimeout(() => {
this.$router.push(this.localePath('/users'))
}, 3000)
} catch (error: any) {
this.showError = true
this.errorMessage = error.message || this.$t('errors.invalidUserOrPass')
this.showSuccess = false
this.errorMessage = error.message || 'Erro ao registar utilizador. Verifique os dados inseridos.'
}
},
cancelRegister() {
this.$router.go(-1)
}
}
})

2
frontend/components/user/UserList.vue

@ -94,6 +94,8 @@ export default Vue.extend({
headers(): { text: any; value: string; sortable?: boolean; align?: string }[] {
return [
{ text: this.$t('user.username'), value: 'username' },
{ text: 'Nome', value: 'firstName' },
{ text: 'Apelido', value: 'lastName' },
{ text: this.$t('user.email'), value: 'email' },
{ text: this.$t('user.superUser'), value: 'isSuperUser', align: 'center' },
{ text: this.$t('user.staff'), value: 'isStaff', align: 'center' },

6
frontend/domain/models/user/user.ts

@ -2,6 +2,8 @@ export class User {
constructor(
readonly id: number,
readonly username: string,
readonly firstName: string,
readonly lastName: string,
readonly email: string,
readonly isActive: boolean,
readonly isSuperUser: boolean,
@ -15,9 +17,9 @@ export class UserDetails extends User {
constructor(
readonly id: number,
readonly username: string,
readonly email: string,
readonly firstName: string,
readonly lastName: string,
readonly email: string,
readonly isActive: boolean,
readonly isSuperUser: boolean,
readonly isStaff: boolean,
@ -25,6 +27,6 @@ export class UserDetails extends User {
readonly groups?: number[],
readonly groupsDetails?: { [key: string]: { name: string } }
) {
super(id, username, email, isActive, isSuperUser, isStaff, groups, groupsDetails)
super(id, username, firstName, lastName, email, isActive, isSuperUser, isStaff, groups, groupsDetails)
}
}

2
frontend/i18n/en/rules.js

@ -13,7 +13,7 @@ export default {
userNameRules: {
userNameRequired: 'User name is required',
userNameLessThan30Chars: 'User name must be less than 30 characters',
min : 'User name must be at least 3 characters long',
minLength : 'User name must be at least 3 characters long',
},
roleRules: {
roleRequired: 'Role is required'

49
frontend/mixins/databaseHealthMixin.js

@ -0,0 +1,49 @@
import DatabaseService from '@/services/database.service'
export const databaseHealthMixin = {
data() {
return {
isDatabaseHealthy: true,
databaseMessage: '',
healthCheckInterval: null
}
},
mounted() {
this.startHealthCheck()
},
beforeDestroy() {
this.stopHealthCheck()
},
methods: {
async checkDatabaseHealth() {
try {
const result = await DatabaseService.checkHealth()
this.isDatabaseHealthy = result.isHealthy
this.databaseMessage = result.message
} catch (error) {
this.isDatabaseHealthy = false
this.databaseMessage = 'Base de dados não disponível, por favor tente mais tarde.'
}
},
startHealthCheck() {
// Verificação inicial
this.checkDatabaseHealth()
// Verificação a cada 10 segundos
this.healthCheckInterval = setInterval(() => {
this.checkDatabaseHealth()
}, 10000)
},
stopHealthCheck() {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval)
this.healthCheckInterval = null
}
}
}
}

11
frontend/repositories/auth/apiAuthRepository.ts

@ -21,11 +21,20 @@ export class APIAuthRepository {
async register(
username: string,
firstName: string,
lastName: string,
email: string,
password: string,
passwordConfirm: string
): Promise<void> {
const url = '/register'
await this.request.post(url, { username, email, password, password_confirm: passwordConfirm })
await this.request.post(url, {
username,
first_name: firstName,
last_name: lastName,
email,
password,
password_confirm: passwordConfirm
})
}
}

7
frontend/repositories/user/apiUserRepository.ts

@ -12,6 +12,8 @@ function toModel(item: { [key: string]: any }): User {
return new User(
item.id,
item.username,
item.first_name || '',
item.last_name || '',
item.email,
item.is_active,
item.is_superuser,
@ -23,12 +25,11 @@ function toModel(item: { [key: string]: any }): User {
function toModelDetails(item: { [key: string]: any }): UserDetails {
return new UserDetails(
item.id,
item.username,
item.first_name || '',
item.last_name || '',
item.email,
item.first_name,
item.last_name,
item.is_active,
item.is_superuser,
item.is_staff,

26
frontend/services/database.service.js

@ -0,0 +1,26 @@
import ApiService from './api.service'
class DatabaseService {
async checkHealth() {
try {
console.log('DatabaseService: Calling /database/health endpoint')
const response = await ApiService.get('/database/health')
console.log('DatabaseService: Response received:', response.data)
return {
isHealthy: response.data.status === 'healthy',
message: response.data.message,
status: response.data.status
}
} catch (error) {
console.error('DatabaseService: Error calling health endpoint:', error)
console.error('DatabaseService: Error response:', error.response)
return {
isHealthy: false,
message: 'Base de dados não disponível, por favor tente mais tarde.',
status: 'unhealthy'
}
}
}
}
export default new DatabaseService()

53
frontend/store/auth.js

@ -81,12 +81,63 @@ export const actions = {
try {
await this.$repositories.auth.register(
authData.username,
authData.firstName,
authData.lastName,
authData.email,
authData.password,
authData.passwordConfirm
)
} catch (error) {
throw new Error('Failed to register user')
console.log('Register error:', error)
// Se temos dados de resposta do servidor, vamos processar os erros
if (error.response && error.response.data) {
const errors = error.response.data
let errorMessage = ''
// Verificar erros específicos de cada campo
if (errors.password) {
const passwordErrors = Array.isArray(errors.password) ? errors.password : [errors.password]
for (const passwordError of passwordErrors) {
if (typeof passwordError === 'string') {
if (passwordError.includes('too common') || passwordError.includes('common')) {
errorMessage = 'A palavra-passe é muito comum. Use uma palavra-passe mais segura.'
} else if (passwordError.includes('too short')) {
errorMessage = 'A palavra-passe é muito curta. Use pelo menos 8 caracteres.'
} else if (passwordError.includes('numeric')) {
errorMessage = 'A palavra-passe não pode ser apenas numérica.'
} else if (passwordError.includes('similar')) {
errorMessage = 'A palavra-passe é muito semelhante às suas informações pessoais.'
} else {
errorMessage = `Erro na palavra-passe: ${passwordError}`
}
break
}
}
} else if (errors.username) {
const usernameErrors = Array.isArray(errors.username) ? errors.username : [errors.username]
if (usernameErrors[0].includes('already exists') || usernameErrors[0].includes('username already exists')) {
errorMessage = 'Este username já está em uso. Use um username diferente.'
} else {
errorMessage = `Erro no nome de utilizador: ${usernameErrors[0]}`
}
} else if (errors.email) {
const emailErrors = Array.isArray(errors.email) ? errors.email : [errors.email]
if (emailErrors[0].includes('already exists')) {
errorMessage = 'Este email já está em uso. Use um email diferente.'
} else {
errorMessage = `Erro no email: ${emailErrors[0]}`
}
} else if (errors.password_confirm) {
errorMessage = 'As palavras-passe não coincidem.'
} else {
errorMessage = 'Falha ao registar utilizador. Verifique os dados inseridos.'
}
throw new Error(errorMessage)
}
throw new Error('Falha ao registar utilizador. Verifique a ligação ao servidor.')
}
}
}

4
yarn.lock

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
Loading…
Cancel
Save