feat : add User CRUD with admin management
Add User API operations (GET, POST, PATCH, DELETE) with password hashing processor, frontend service, drawer and admin tab. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
106
frontend/components/AdminUserTab.vue
Normal file
106
frontend/components/AdminUserTab.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-bold text-neutral-900">Utilisateurs</h2>
|
||||
<button
|
||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
||||
@click="openCreate"
|
||||
>
|
||||
+ Ajouter un utilisateur
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 overflow-x-auto rounded-lg border border-neutral-200">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-neutral-200 bg-neutral-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 font-semibold text-neutral-700">Nom d'utilisateur</th>
|
||||
<th class="px-4 py-3 font-semibold text-neutral-700">Rôles</th>
|
||||
<th class="px-4 py-3 font-semibold text-neutral-700">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
class="cursor-pointer border-b border-neutral-100 hover:bg-neutral-50"
|
||||
@click="openEdit(item)"
|
||||
>
|
||||
<td class="px-4 py-3 font-semibold text-primary-500">{{ item.username }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<span
|
||||
v-for="role in item.roles"
|
||||
:key="role"
|
||||
class="mr-1 rounded-full bg-neutral-200 px-2 py-0.5 text-xs font-semibold text-neutral-700"
|
||||
>
|
||||
{{ role }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<button
|
||||
class="text-red-500 hover:text-red-700"
|
||||
@click.stop="handleDelete(item.id)"
|
||||
>
|
||||
<Icon name="mdi:delete-outline" size="20" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="items.length === 0 && !isLoading">
|
||||
<td colspan="3" class="px-4 py-8 text-center text-neutral-400">
|
||||
Aucun utilisateur trouvé.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<UserDrawer
|
||||
v-model="drawerOpen"
|
||||
:item="selectedItem"
|
||||
@saved="onSaved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
import { useUserService } from '~/services/users'
|
||||
|
||||
const { getAll, remove } = useUserService()
|
||||
const items = ref<UserData[]>([])
|
||||
const isLoading = ref(true)
|
||||
const drawerOpen = ref(false)
|
||||
const selectedItem = ref<UserData | null>(null)
|
||||
|
||||
async function loadItems() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
items.value = await getAll()
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
selectedItem.value = null
|
||||
drawerOpen.value = true
|
||||
}
|
||||
|
||||
function openEdit(item: UserData) {
|
||||
selectedItem.value = item
|
||||
drawerOpen.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
await remove(id)
|
||||
await loadItems()
|
||||
}
|
||||
|
||||
async function onSaved() {
|
||||
await loadItems()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadItems()
|
||||
})
|
||||
</script>
|
||||
133
frontend/components/UserDrawer.vue
Normal file
133
frontend/components/UserDrawer.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un utilisateur' : 'Ajouter un utilisateur'">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="handleSubmit">
|
||||
<MalioInputText
|
||||
v-model="form.username"
|
||||
label="Nom d'utilisateur"
|
||||
input-class="w-full"
|
||||
:error="touched.username && !form.username.trim() ? 'Le nom est requis' : ''"
|
||||
@blur="touched.username = true"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="form.password"
|
||||
label="Mot de passe"
|
||||
input-class="w-full"
|
||||
type="password"
|
||||
:placeholder="isEditing ? 'Laisser vide pour ne pas changer' : ''"
|
||||
:error="touched.password && !isEditing && !form.password ? 'Le mot de passe est requis' : ''"
|
||||
@blur="touched.password = true"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<label class="text-sm font-semibold text-neutral-700">Rôles</label>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<label
|
||||
v-for="role in availableRoles"
|
||||
:key="role"
|
||||
class="flex items-center gap-2 text-sm text-neutral-700"
|
||||
>
|
||||
<input
|
||||
v-model="form.roles"
|
||||
type="checkbox"
|
||||
:value="role"
|
||||
class="rounded border-neutral-300"
|
||||
/>
|
||||
{{ role }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AppDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UserData, UserWrite } from '~/services/dto/user-data'
|
||||
import { useUserService } from '~/services/users'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
item: UserData | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'saved'): void
|
||||
}>()
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v),
|
||||
})
|
||||
|
||||
const availableRoles = ['ROLE_ADMIN', 'ROLE_USER']
|
||||
|
||||
const isEditing = computed(() => !!props.item)
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
roles: [] as string[],
|
||||
})
|
||||
|
||||
const touched = reactive({
|
||||
username: false,
|
||||
password: false,
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (open) {
|
||||
if (props.item) {
|
||||
form.username = props.item.username ?? ''
|
||||
form.password = ''
|
||||
form.roles = [...props.item.roles]
|
||||
} else {
|
||||
form.username = ''
|
||||
form.password = ''
|
||||
form.roles = ['ROLE_USER']
|
||||
}
|
||||
touched.username = false
|
||||
touched.password = false
|
||||
}
|
||||
})
|
||||
|
||||
const { create, update } = useUserService()
|
||||
|
||||
async function handleSubmit() {
|
||||
touched.username = true
|
||||
touched.password = true
|
||||
if (!form.username.trim()) return
|
||||
if (!isEditing.value && !form.password) return
|
||||
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
const payload: UserWrite = {
|
||||
username: form.username.trim(),
|
||||
roles: form.roles,
|
||||
}
|
||||
if (form.password) {
|
||||
payload.password = form.password
|
||||
}
|
||||
|
||||
if (isEditing.value && props.item) {
|
||||
await update(props.item.id, payload)
|
||||
} else {
|
||||
await create(payload)
|
||||
}
|
||||
|
||||
emit('saved')
|
||||
isOpen.value = false
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user