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

@ -0,0 +1,358 @@
// API Response structure for user list
export interface AdminUserResponse {
id: number;
username: string;
email: string;
created_at: string;
updated_at: string;
role: {
code: string;
name: {
en: string;
th: string;
};
};
profile: {
prefix: {
en: string;
th: string;
};
first_name: string;
last_name: string;
avatar_url: string | null;
birth_date: string | null;
phone: string | null;
};
}
export interface UsersListResponse {
code: number;
message: string;
data: AdminUserResponse[];
}
// Mock data for development
const MOCK_USERS: AdminUserResponse[] = [
{
id: 1,
username: 'admin',
email: 'admin@elearning.local',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
role: { code: 'ADMIN', name: { en: 'Administrator', th: 'ผู้ดูแลระบบ' } },
profile: {
prefix: { en: 'Mr.', th: 'นาย' },
first_name: 'Admin',
last_name: 'User',
avatar_url: null,
birth_date: null,
phone: '0812345678'
}
},
{
id: 2,
username: 'instructor',
email: 'instructor@elearning.local',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
role: { code: 'INSTRUCTOR', name: { en: 'Instructor', th: 'ผู้สอน' } },
profile: {
prefix: { en: 'Mr.', th: 'นาย' },
first_name: 'John',
last_name: 'Instructor',
avatar_url: null,
birth_date: null,
phone: '0812345679'
}
},
{
id: 3,
username: 'student',
email: 'student@elearning.local',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
role: { code: 'STUDENT', name: { en: 'Student', th: 'นักเรียน' } },
profile: {
prefix: { en: 'Ms.', th: 'นางสาว' },
first_name: 'Jane',
last_name: 'Student',
avatar_url: null,
birth_date: null,
phone: '0812345680'
}
}
];
// Helper function to get auth token from cookie or localStorage
const getAuthToken = (): string => {
const tokenCookie = useCookie('token');
return tokenCookie.value || '';
};
export const adminService = {
async getUsers(): Promise<AdminUserResponse[]> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_USERS;
}
const token = getAuthToken();
const response = await $fetch<UsersListResponse>('/api/admin/usermanagement/users', {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data;
},
async getUserById(id: number): Promise<AdminUserResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 300));
const user = MOCK_USERS.find(u => u.id === id);
if (!user) throw new Error('User not found');
return user;
}
const token = getAuthToken();
const response = await $fetch<AdminUserResponse>(`/api/admin/usermanagement/users/${id}`, {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response;
},
async updateUserRole(userId: number, roleId: number): Promise<void> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return;
}
const token = getAuthToken();
await $fetch(`/api/admin/usermanagement/role/${userId}`, {
method: 'PUT',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: {
id: userId,
role_id: roleId
}
});
},
async deleteUser(userId: number): Promise<void> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return;
}
const token = getAuthToken();
await $fetch(`/api/admin/usermanagement/users/${userId}`, {
method: 'DELETE',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
},
// ============ Categories ============
async getCategories(): Promise<CategoryResponse[]> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_CATEGORIES;
}
const token = getAuthToken();
const response = await $fetch<CategoriesListResponse>('/api/admin/categories', {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data.categories;
},
async createCategory(data: CreateCategoryRequest): Promise<CategoryResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return { ...MOCK_CATEGORIES[0], id: Date.now() };
}
const token = getAuthToken();
const response = await $fetch<CategoryResponse>('/api/admin/categories', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: data
});
return response;
},
async updateCategory(id: number, data: UpdateCategoryRequest): Promise<CategoryResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return { ...MOCK_CATEGORIES[0], id };
}
const token = getAuthToken();
const response = await $fetch<CategoryResponse>(`/api/admin/categories/${id}`, {
method: 'PUT',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: data
});
return response;
},
async deleteCategory(id: number): Promise<void> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return;
}
const token = getAuthToken();
await $fetch(`/api/admin/categories/${id}`, {
method: 'DELETE',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
}
};
// Category interfaces
export interface CategoryResponse {
id: number;
name: {
en: string;
th: string;
};
slug: string;
description: {
en: string;
th: string;
};
icon: string;
sort_order: number;
is_active: boolean;
created_at: string;
created_by: number;
updated_at: string;
updated_by: number | null;
}
export interface CategoriesListResponse {
code: number;
message: string;
data: {
categories: CategoryResponse[];
};
}
export interface CreateCategoryRequest {
name: {
en: string;
th: string;
};
slug: string;
description: {
en: string;
th: string;
};
created_by?: number;
}
export interface UpdateCategoryRequest {
id: number;
name: {
en: string;
th: string;
};
slug: string;
description: {
en: string;
th: string;
};
}
// Mock categories
const MOCK_CATEGORIES: CategoryResponse[] = [
{
id: 1,
name: { en: 'Web Development', th: 'การพัฒนาเว็บไซต์' },
slug: 'web-development',
description: { en: 'Learn web development', th: 'หลักสูตรเกี่ยวกับการพัฒนาเว็บไซต์และเว็บแอปพลิเคชัน' },
icon: 'code',
sort_order: 1,
is_active: true,
created_at: '2024-01-15T00:00:00Z',
created_by: 1,
updated_at: '2024-01-15T00:00:00Z',
updated_by: null
},
{
id: 2,
name: { en: 'Mobile Development', th: 'การพัฒนาแอปพลิเคชันมือถือ' },
slug: 'mobile-development',
description: { en: 'Learn mobile app development', th: 'หลักสูตรเกี่ยวกับการพัฒนาแอปพลิเคชันบนมือถือ' },
icon: 'smartphone',
sort_order: 2,
is_active: true,
created_at: '2024-01-20T00:00:00Z',
created_by: 1,
updated_at: '2024-01-20T00:00:00Z',
updated_by: null
},
{
id: 3,
name: { en: 'Database', th: 'ฐานข้อมูล' },
slug: 'database',
description: { en: 'Learn database management', th: 'หลักสูตรเกี่ยวกับการออกแบบและจัดการฐานข้อมูล' },
icon: 'storage',
sort_order: 3,
is_active: true,
created_at: '2024-02-01T00:00:00Z',
created_by: 1,
updated_at: '2024-02-01T00:00:00Z',
updated_by: null
}
];

View file

@ -41,14 +41,72 @@ export interface LoginResponse {
user: {
id: string;
email: string;
fullName: string;
firstName: string;
lastName: string;
role: string;
};
}
// Mock data for development
const MOCK_USERS = [
{
email: 'admin@elearning.local',
password: 'password',
user: {
id: '1',
email: 'admin@elearning.local',
firstName: 'ผู้ดูแล',
lastName: 'ระบบ',
role: 'ADMIN'
}
},
{
email: 'instructor@elearning.local',
password: 'password',
user: {
id: '2',
email: 'instructor@elearning.local',
firstName: 'อาจารย์',
lastName: 'ทดสอบ',
role: 'INSTRUCTOR'
}
}
];
export const authService = {
async login(email: string, password: string): Promise<LoginResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
// Use Mock Data
if (useMockData) {
return this.mockLogin(email, password);
}
// Use Real API
return this.apiLogin(email, password);
},
// Mock login for development
async mockLogin(email: string, password: string): Promise<LoginResponse> {
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate delay
const mockUser = MOCK_USERS.find(u => u.email === email);
if (!mockUser || mockUser.password !== password) {
throw new Error('อีเมลหรือรหัสผ่านไม่ถูกต้อง');
}
return {
token: 'mock-jwt-token',
refreshToken: 'mock-refresh-token',
user: mockUser.user
};
},
// Real API login
async apiLogin(email: string, password: string): Promise<LoginResponse> {
const config = useRuntimeConfig();
try {
const response = await $fetch<ApiLoginResponse>('/api/auth/login', {
@ -60,6 +118,11 @@ export const authService = {
}
});
// Check if user role is STUDENT - block login
if (response.user.role.code === 'STUDENT') {
throw new Error('ไม่สามารถเข้าสู่ระบบได้ ระบบนี้สำหรับผู้สอนและผู้ดูแลระบบเท่านั้น');
}
// Transform API response to frontend format
return {
token: response.token,
@ -67,26 +130,68 @@ export const authService = {
user: {
id: response.user.id.toString(),
email: response.user.email,
fullName: `${response.user.profile.first_name} ${response.user.profile.last_name}`,
role: response.user.role.code // Use role.code (ADMIN, INSTRUCTOR, etc.)
firstName: response.user.profile.first_name,
lastName: response.user.profile.last_name,
role: response.user.role.code
}
};
} catch (error: any) {
// Re-throw custom errors (like STUDENT role block)
if (error.message && !error.response) {
throw error;
}
// Handle API errors
if (error.response?.status === 401) {
throw new Error('Invalid credentials');
throw new Error('อีเมลหรือรหัสผ่านไม่ถูกต้อง');
}
throw new Error('Login failed');
throw new Error('เกิดข้อผิดพลาดในการเข้าสู่ระบบ');
}
},
async logout(): Promise<void> {
// TODO: Call logout API if available
// For now, just clear local storage
if (process.client) {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user');
// Clear cookies
const tokenCookie = useCookie('token');
const refreshTokenCookie = useCookie('refreshToken');
const userCookie = useCookie('user');
tokenCookie.value = null;
refreshTokenCookie.value = null;
userCookie.value = null;
},
async forgotPassword(email: string): Promise<void> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
// Mock: simulate sending email
await new Promise(resolve => setTimeout(resolve, 1000));
return;
}
// Real API
await $fetch('/api/auth/reset-request', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
body: { email }
});
},
async resetPassword(token: string, password: string): Promise<void> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
// Mock: simulate reset password
await new Promise(resolve => setTimeout(resolve, 1000));
return;
}
// Real API
await $fetch('/api/auth/reset-password', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
body: { token, password }
});
}
};

View file

@ -0,0 +1,182 @@
// API Response structure from /api/user/me
export interface UserProfileResponse {
id: number;
username: string;
email: string;
updated_at: string;
created_at: string;
role: {
code: string;
name: {
en: string;
th: string;
};
};
profile: {
prefix: {
en: string;
th: string;
};
first_name: string;
last_name: string;
avatar_url: string | null;
birth_date: string | null;
phone: string | null;
};
}
// Request body for PUT /api/user/me
export interface UpdateProfileRequest {
prefix?: {
en: string;
th: string;
};
first_name: string;
last_name: string;
phone?: string | null;
avatar_url?: string | null;
birth_date?: string | null;
}
// Mock profile data for development
const MOCK_PROFILES: Record<string, UserProfileResponse> = {
'admin@elearning.local': {
id: 1,
username: 'admin',
email: 'admin@elearning.local',
updated_at: '2024-01-01T00:00:00Z',
created_at: '2024-01-01T00:00:00Z',
role: {
code: 'ADMIN',
name: { en: 'Administrator', th: 'ผู้ดูแลระบบ' }
},
profile: {
prefix: { en: 'Mr.', th: 'นาย' },
first_name: 'ผู้ดูแล',
last_name: 'ระบบ',
avatar_url: null,
birth_date: null,
phone: '081-234-5678'
}
},
'instructor@elearning.local': {
id: 2,
username: 'instructor',
email: 'instructor@elearning.local',
updated_at: '2024-01-01T00:00:00Z',
created_at: '2024-01-01T00:00:00Z',
role: {
code: 'INSTRUCTOR',
name: { en: 'Instructor', th: 'ผู้สอน' }
},
profile: {
prefix: { en: 'Mr.', th: 'นาย' },
first_name: 'อาจารย์',
last_name: 'ทดสอบ',
avatar_url: null,
birth_date: null,
phone: '081-234-5678'
}
}
};
// Helper function to get auth token from cookie
const getAuthToken = (): string => {
const tokenCookie = useCookie('token');
return tokenCookie.value || '';
};
export const userService = {
async getProfile(): Promise<UserProfileResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
return this.mockGetProfile();
}
return this.apiGetProfile();
},
// Mock get profile
async mockGetProfile(): Promise<UserProfileResponse> {
await new Promise(resolve => setTimeout(resolve, 300));
const userCookie = useCookie('user');
if (!userCookie.value) throw new Error('User not found');
const user = typeof userCookie.value === 'string'
? JSON.parse(userCookie.value)
: userCookie.value;
const mockProfile = MOCK_PROFILES[user.email];
if (!mockProfile) {
throw new Error('Profile not found');
}
return mockProfile;
},
// Real API get profile
async apiGetProfile(): Promise<UserProfileResponse> {
const config = useRuntimeConfig();
const token = getAuthToken();
const response = await $fetch<UserProfileResponse>('/api/user/me', {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
return response;
},
async updateProfile(data: UpdateProfileRequest): Promise<UserProfileResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
// In mock mode, just return the current profile with updates
const profile = await this.mockGetProfile();
return { ...profile, profile: { ...profile.profile, ...data } };
}
const token = getAuthToken();
const response = await $fetch<UserProfileResponse>('/api/user/me', {
method: 'PUT',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: data
});
return response;
},
async changePassword(oldPassword: string, newPassword: string): Promise<void> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
// In mock mode, just simulate success
return;
}
const token = getAuthToken();
await $fetch('/api/user/change-password', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: {
oldPassword,
newPassword
}
});
}
};