feat: Implement user authentication, admin user management, and role-based access control.

This commit is contained in:
Missez 2026-01-16 16:37:16 +07:00
parent 8a2ca592bc
commit 38648581ec
19 changed files with 1762 additions and 514 deletions

View file

@ -5,13 +5,13 @@
<div class="flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold text-gray-900">
สวสด, {{ authStore.user?.fullName || 'อาจารย์' }}
สวสด, {{ authStore.user?.firstName }} {{ authStore.user?.lastName || 'อาจารย์' }}
</h1>
<p class="text-gray-600 mt-2">นดอนรบกลบสระบบ</p>
</div>
<div class="flex items-center gap-4">
<div class="text-right">
<div class="text-sm font-semibold text-gray-900">{{ authStore.user?.fullName || 'อาจารย์ทดสอบ' }}</div>
<div class="text-sm font-semibold text-gray-900">{{ authStore.user?.firstName }} {{ authStore.user?.lastName || 'อาจารย์ทดสอบ' }}</div>
<div class="text-xs text-gray-500">{{ authStore.user?.role || 'INSTRUCTOR' }}</div>
</div>
<div
@ -23,7 +23,7 @@
<!-- User Info Header -->
<q-item class="bg-primary-50">
<q-item-section>
<q-item-label class="text-weight-bold">{{ authStore.user?.fullName }}</q-item-label>
<q-item-label class="text-weight-bold">{{ authStore.user?.firstName }} {{ authStore.user?.lastName }}</q-item-label>
<q-item-label caption>{{ authStore.user?.email }}</q-item-label>
</q-item-section>
</q-item>

View file

@ -7,7 +7,7 @@
</div>
<!-- Profile Card -->
<AppCard class="mb-10 ">
<div class="mb-10 ">
<div class="flex flex-col md:flex-row gap-8">
<!-- Avatar Section -->
<div class="flex flex-col items-center">
@ -49,7 +49,7 @@
<div>
<div class="text-sm text-gray-600 mb-1">ตำแหน</div>
<div class="text-lg text-gray-900">
<q-badge color="primary">{{ getRoleLabel(profile.role) }}</q-badge>
<q-badge color="primary">{{ profile.roleName || getRoleLabel(profile.role) }}</q-badge>
</div>
</div>
@ -77,7 +77,7 @@
</div>
</div>
</div>
</AppCard>
</div>
<!-- Stats Cards
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mt-5">
@ -108,11 +108,11 @@
<q-card-section>
<q-form @submit="handleUpdateProfile">
<q-input
v-model="editForm.fullName"
label="ชื่อ-นามสกุล"
v-model="editForm.firstName"
label="ชื่อ"
outlined
class="mb-4"
:rules="[val => !!val || 'กรุณากรอกชื่อ-นามสกุล']"
:rules="[val => !!val || 'กรุณากรอกชื่อ']"
>
<template v-slot:prepend>
<q-icon name="person" />
@ -120,27 +120,14 @@
</q-input>
<q-input
v-model="editForm.email"
label="อีเมล"
type="email"
v-model="editForm.lastName"
label="นามสกุล"
outlined
class="mb-4"
:rules="[val => !!val || 'กรุณากรอกอีเมล']"
:rules="[val => !!val || 'กรุณากรอกนามสกุล']"
>
<template v-slot:prepend>
<q-icon name="email" />
</template>
</q-input>
<q-input
v-model="editForm.username"
label="Username"
outlined
class="mb-4"
:rules="[val => !!val || 'กรุณากรอก username']"
>
<template v-slot:prepend>
<q-icon name="account_circle" />
<q-icon name="person" />
</template>
</q-input>
@ -274,6 +261,7 @@
<script setup lang="ts">
import { useQuasar } from 'quasar';
import { userService, type UserProfileResponse } from '~/services/user.service';
definePageMeta({
layout: 'instructor',
@ -283,15 +271,19 @@ definePageMeta({
const $q = useQuasar();
const authStore = useAuthStore();
// Loading state
const loading = ref(true);
// Profile data
const profile = ref({
fullName: 'อาจารย์ทดสอบ ระบบอีเลินนิ่ง',
email: 'instructor@example.com',
username: 'instructor_test',
phone: '081-234-5678',
role: 'INSTRUCTOR',
fullName: '',
email: '',
username: '',
phone: '',
role: '',
roleName: '',
avatar: '👨‍🏫',
createdAt: '2024-01-01'
createdAt: ''
});
const stats = ref({
@ -303,9 +295,8 @@ const stats = ref({
const showEditModal = ref(false);
const saving = ref(false);
const editForm = ref({
fullName: '',
email: '',
username: '',
firstName: '',
lastName: '',
phone: ''
});
@ -351,11 +342,15 @@ const handleUpdateProfile = async () => {
saving.value = true;
try {
// TODO: Call API to update profile
await new Promise(resolve => setTimeout(resolve, 1000));
// Call real API to update profile
await userService.updateProfile({
first_name: editForm.value.firstName,
last_name: editForm.value.lastName,
phone: editForm.value.phone || null
});
// Update local data
profile.value = { ...profile.value, ...editForm.value };
// Refresh profile data from API
await fetchProfile();
$q.notify({
type: 'positive',
@ -364,7 +359,7 @@ const handleUpdateProfile = async () => {
});
showEditModal.value = false;
} catch (error) {
} catch (error: any) {
$q.notify({
type: 'negative',
message: 'เกิดข้อผิดพลาด กรุณาลองใหม่อีกครั้ง',
@ -379,8 +374,11 @@ const handleChangePassword = async () => {
changingPassword.value = true;
try {
// TODO: Call API to change password
await new Promise(resolve => setTimeout(resolve, 1000));
// Call real API to change password
await userService.changePassword(
passwordForm.value.currentPassword,
passwordForm.value.newPassword
);
$q.notify({
type: 'positive',
@ -394,10 +392,12 @@ const handleChangePassword = async () => {
newPassword: '',
confirmPassword: ''
};
} catch (error) {
} catch (error: any) {
$q.notify({
type: 'negative',
message: 'รหัสผ่านปัจจุบันไม่ถูกต้อง',
message: error.response?.status === 401
? 'รหัสผ่านปัจจุบันไม่ถูกต้อง'
: 'เกิดข้อผิดพลาดในการเปลี่ยนรหัสผ่าน',
position: 'top'
});
} finally {
@ -408,12 +408,46 @@ const handleChangePassword = async () => {
// Watch edit modal
watch(showEditModal, (newVal) => {
if (newVal) {
// Split fullName into firstName and lastName for editing
const nameParts = profile.value.fullName.split(' ');
editForm.value = {
fullName: profile.value.fullName,
email: profile.value.email,
username: profile.value.username,
firstName: nameParts[0] || '',
lastName: nameParts.slice(1).join(' ') || '',
phone: profile.value.phone
};
}
});
// Fetch profile from API
const fetchProfile = async () => {
loading.value = true;
try {
const data = await userService.getProfile();
// Map API response to profile
profile.value = {
fullName: `${data.profile.first_name} ${data.profile.last_name}`,
email: data.email,
username: data.username,
phone: data.profile.phone || '',
role: data.role.code,
roleName: data.role.name.th,
avatar: data.profile.avatar_url || '👨‍🏫',
createdAt: data.created_at
};
} catch (error) {
$q.notify({
type: 'negative',
message: 'ไม่สามารถโหลดข้อมูลโปรไฟล์ได้',
position: 'top'
});
} finally {
loading.value = false;
}
};
// Load profile on mount
onMounted(() => {
fetchProfile();
});
</script>