feat: Implement authentication system with token refresh and initial instructor dashboard with course management.
This commit is contained in:
parent
0eb9b522f6
commit
ab3124628c
11 changed files with 1053 additions and 93 deletions
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue