feat: Implement authentication system with token refresh and initial instructor dashboard with course management.

This commit is contained in:
Missez 2026-01-23 09:53:39 +07:00
parent 0eb9b522f6
commit ab3124628c
11 changed files with 1053 additions and 93 deletions

View file

@ -211,6 +211,33 @@ export const authService = {
baseURL: config.public.apiBaseUrl as string,
body: data
});
},
async refreshToken(currentRefreshToken: string): Promise<{ token: string; refreshToken: string }> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
// Mock: return new tokens
await new Promise(resolve => setTimeout(resolve, 500));
return {
token: 'mock-new-jwt-token-' + Date.now(),
refreshToken: 'mock-new-refresh-token-' + Date.now()
};
}
if (!currentRefreshToken) {
throw new Error('No refresh token available');
}
// Real API
const response = await $fetch<{ token: string; refreshToken: string }>('/api/auth/refresh', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
body: { refreshToken: currentRefreshToken }
});
return response;
}
};

View file

@ -38,6 +38,45 @@ const getAuthToken = (): string => {
return tokenCookie.value || '';
};
// Helper function for making authenticated requests with auto refresh
const authRequest = async <T>(
url: string,
options: {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: any;
} = {}
): Promise<T> => {
const config = useRuntimeConfig();
const makeRequest = async (token: string) => {
return await $fetch<T>(url, {
baseURL: config.public.apiBaseUrl as string,
method: options.method || 'GET',
headers: {
Authorization: `Bearer ${token}`
},
body: options.body
});
};
try {
const token = getAuthToken();
return await makeRequest(token);
} catch (error: any) {
// If 401, try to refresh token
if (error.response?.status === 401) {
const { handleUnauthorized } = await import('~/utils/authFetch');
const newToken = await handleUnauthorized();
if (newToken) {
return await makeRequest(newToken);
}
// Redirect to login
navigateTo('/login');
}
throw error;
}
};
// Mock courses data
const MOCK_COURSES: CourseResponse[] = [
{
@ -118,14 +157,7 @@ export const instructorService = {
return MOCK_COURSES;
}
const token = getAuthToken();
const response = await $fetch<CoursesListResponse>('/api/instructors/courses', {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
const response = await authRequest<CoursesListResponse>('/api/instructors/courses');
return response.data;
},
@ -146,27 +178,16 @@ export const instructorService = {
} as CourseResponse;
}
const token = getAuthToken();
// Clean data - remove empty thumbnail_url
const cleanedData = { ...data };
if (!cleanedData.thumbnail_url) {
delete cleanedData.thumbnail_url;
}
console.log('=== CREATE COURSE DEBUG ===');
console.log('Body:', JSON.stringify({ data: cleanedData }, null, 2));
console.log('===========================');
const response = await $fetch<{ code: number; data: CourseResponse }>('/api/instructors/courses', {
const response = await authRequest<{ code: number; data: CourseResponse }>('/api/instructors/courses', {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: { data: cleanedData }
});
return response.data;
},
@ -179,14 +200,7 @@ export const instructorService = {
return MOCK_COURSE_DETAIL;
}
const token = getAuthToken();
const response = await $fetch<{ code: number; data: CourseDetailResponse }>(`/api/instructors/courses/${courseId}`, {
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
const response = await authRequest<{ code: number; data: CourseDetailResponse }>(`/api/instructors/courses/${courseId}`);
return response.data;
},
@ -204,23 +218,10 @@ export const instructorService = {
} as CourseResponse;
}
const token = getAuthToken();
// Debug log
console.log('=== UPDATE COURSE DEBUG ===');
console.log('URL:', `${config.public.apiBaseUrl}/api/instructors/courses/${courseId}`);
console.log('Body:', JSON.stringify({ data }, null, 2));
console.log('===========================');
const response = await $fetch<{ code: number; data: CourseResponse }>(`/api/instructors/courses/${courseId}`, {
const response = await authRequest<{ code: number; data: CourseResponse }>(`/api/instructors/courses/${courseId}`, {
method: 'PUT',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
},
body: { data }
});
return response.data;
},
@ -233,14 +234,7 @@ export const instructorService = {
return;
}
const token = getAuthToken();
await $fetch(`/api/instructors/courses/${courseId}`, {
method: 'DELETE',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
await authRequest(`/api/instructors/courses/${courseId}`, { method: 'DELETE' });
},
async sendForReview(courseId: number): Promise<void> {
@ -252,14 +246,93 @@ export const instructorService = {
return;
}
const token = getAuthToken();
await $fetch(`/api/instructors/courses/send-review/${courseId}`, {
method: 'POST',
baseURL: config.public.apiBaseUrl as string,
headers: {
Authorization: `Bearer ${token}`
}
});
await authRequest(`/api/instructors/courses/send-review/${courseId}`, { method: 'POST' });
},
async getChapters(courseId: number): Promise<ChapterResponse[]> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_COURSE_DETAIL.chapters;
}
const response = await authRequest<{ code: number; data: ChapterResponse[]; total: number }>(
`/api/instructors/courses/${courseId}/chapters`
);
return response.data;
},
async createChapter(courseId: number, data: CreateChapterRequest): Promise<ChapterResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
...MOCK_COURSE_DETAIL.chapters[0],
id: Date.now(),
...data,
lessons: []
};
}
const response = await authRequest<{ code: number; data: ChapterResponse }>(
`/api/instructors/courses/${courseId}/chapters`,
{ method: 'POST', body: data }
);
return response.data;
},
async updateChapter(courseId: number, chapterId: number, data: CreateChapterRequest): Promise<ChapterResponse> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return {
...MOCK_COURSE_DETAIL.chapters[0],
id: chapterId,
...data
};
}
const response = await authRequest<{ code: number; data: ChapterResponse }>(
`/api/instructors/courses/${courseId}/chapters/${chapterId}`,
{ method: 'PUT', body: data }
);
return response.data;
},
async deleteChapter(courseId: number, chapterId: number): Promise<void> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 500));
return;
}
await authRequest(
`/api/instructors/courses/${courseId}/chapters/${chapterId}`,
{ method: 'DELETE' }
);
},
async reorderChapter(courseId: number, chapterId: number, sortOrder: number): Promise<void> {
const config = useRuntimeConfig();
const useMockData = config.public.useMockData as boolean;
if (useMockData) {
await new Promise(resolve => setTimeout(resolve, 300));
return;
}
await authRequest(
`/api/instructors/courses/${courseId}/chapters/${chapterId}/reorder`,
{ method: 'PUT', body: { sort_order: sortOrder } }
);
}
};
@ -327,6 +400,13 @@ export interface QuizResponse {
show_answers_after_completion: boolean;
}
export interface CreateChapterRequest {
title: { th: string; en: string };
description: { th: string; en: string };
sort_order?: number;
is_published?: boolean;
}
// Mock course detail
const MOCK_COURSE_DETAIL: CourseDetailResponse = {
...MOCK_COURSES[0],