feat: implement auth flow and app initialization
This commit is contained in:
@@ -1,9 +1,28 @@
|
||||
import type { AuthResponseData, LoginPayload } from '@/types';
|
||||
import { axiosAuth } from '../axios/axios';
|
||||
import type { AuthResponseData, LoginPayload, MeResponse, PermissionResponse } from '@/types';
|
||||
import axiosClient, { axiosAuth } from '../axios/axios';
|
||||
|
||||
export const authService = {
|
||||
login: async (payload: LoginPayload): Promise<AuthResponseData> => {
|
||||
const response = await axiosAuth.post<AuthResponseData>('/auth/login', payload);
|
||||
const response = await axiosAuth.post<AuthResponseData>('api/auth/login', payload, {
|
||||
withCredentials: true,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
refresh: async (): Promise<AuthResponseData> => {
|
||||
const response = await axiosAuth.post<AuthResponseData>('api/auth/refresh', {}, {
|
||||
withCredentials: true,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
logout: async (): Promise<void> => {
|
||||
await axiosAuth.post('api/auth/logout', {}, { withCredentials: true });
|
||||
},
|
||||
me: async (): Promise<MeResponse> => {
|
||||
const response = await axiosClient.get<MeResponse>('api/auth/me');
|
||||
return response.data;
|
||||
},
|
||||
permissions: async (): Promise<PermissionResponse> => {
|
||||
const response = await axiosClient.get<PermissionResponse>('api/permissions/my-permissions');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
PaginationParams,
|
||||
} from '@/types';
|
||||
|
||||
const CHAINAGES_ENDPOINT = 'biz/api/v1/chainages';
|
||||
|
||||
/**
|
||||
* Chainage Service
|
||||
*/
|
||||
@@ -17,7 +19,7 @@ export const chainageService = {
|
||||
getChainages: async (params?: PaginationParams): Promise<PaginatedResponse<Chainage>> => {
|
||||
const skip = params?.skip ?? 0;
|
||||
const limit = params?.limit ?? 100;
|
||||
const response = await axiosClient.get<PaginatedResponse<Chainage>>(`/chainages/`, {
|
||||
const response = await axiosClient.get<PaginatedResponse<Chainage>>(CHAINAGES_ENDPOINT, {
|
||||
params: { skip, limit },
|
||||
});
|
||||
return response.data;
|
||||
@@ -32,7 +34,7 @@ export const chainageService = {
|
||||
): Promise<PaginatedResponse<Chainage>> => {
|
||||
const skip = params?.skip ?? 0;
|
||||
const limit = params?.limit ?? 100;
|
||||
const response = await axiosClient.get<PaginatedResponse<Chainage>>(`/chainages/`, {
|
||||
const response = await axiosClient.get<PaginatedResponse<Chainage>>(CHAINAGES_ENDPOINT, {
|
||||
params: { package_id: packageId, skip, limit },
|
||||
});
|
||||
return response.data;
|
||||
@@ -42,7 +44,7 @@ export const chainageService = {
|
||||
* Create a new chainage
|
||||
*/
|
||||
createChainage: async (data: ChainageCreate): Promise<Chainage> => {
|
||||
const response = await axiosClient.post<Chainage>('/chainages/', data);
|
||||
const response = await axiosClient.post<Chainage>(CHAINAGES_ENDPOINT, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -50,7 +52,7 @@ export const chainageService = {
|
||||
* Update an existing chainage
|
||||
*/
|
||||
updateChainage: async (chainageId: string, data: ChainageUpdate): Promise<Chainage> => {
|
||||
const response = await axiosClient.put<Chainage>(`/chainages/${chainageId}`, data);
|
||||
const response = await axiosClient.put<Chainage>(`${CHAINAGES_ENDPOINT}/${chainageId}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -58,7 +60,9 @@ export const chainageService = {
|
||||
* Delete a chainage
|
||||
*/
|
||||
deleteChainage: async (chainageId: string): Promise<{ message: string }> => {
|
||||
const response = await axiosClient.delete<{ message: string }>(`/chainages/${chainageId}`);
|
||||
const response = await axiosClient.delete<{ message: string }>(
|
||||
`${CHAINAGES_ENDPOINT}/${chainageId}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,7 +31,9 @@ export const detectionService = {
|
||||
};
|
||||
};
|
||||
};
|
||||
}>(`/summary/projects/${project.id}`);
|
||||
}>('biz/api/v1/dashboard/overview', {
|
||||
params: { project_id: project.id },
|
||||
});
|
||||
|
||||
const summary = response.data;
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
PaginationParams,
|
||||
} from '@/types';
|
||||
|
||||
const PACKAGES_ENDPOINT = 'biz/api/v1/packages';
|
||||
|
||||
/**
|
||||
* Package Service
|
||||
*/
|
||||
@@ -17,7 +19,7 @@ export const packageService = {
|
||||
getPackages: async (params?: PaginationParams): Promise<PaginatedResponse<Package>> => {
|
||||
const skip = params?.skip ?? 0;
|
||||
const limit = params?.limit ?? 100;
|
||||
const response = await axiosClient.get<PaginatedResponse<Package>>(`/packages/`, {
|
||||
const response = await axiosClient.get<PaginatedResponse<Package>>(PACKAGES_ENDPOINT, {
|
||||
params: { skip, limit },
|
||||
});
|
||||
return response.data;
|
||||
@@ -32,7 +34,7 @@ export const packageService = {
|
||||
): Promise<PaginatedResponse<Package>> => {
|
||||
const skip = params?.skip ?? 0;
|
||||
const limit = params?.limit ?? 100;
|
||||
const response = await axiosClient.get<PaginatedResponse<Package>>(`/packages/`, {
|
||||
const response = await axiosClient.get<PaginatedResponse<Package>>(PACKAGES_ENDPOINT, {
|
||||
params: { project_id: projectId, skip, limit },
|
||||
});
|
||||
return response.data;
|
||||
@@ -42,7 +44,7 @@ export const packageService = {
|
||||
* Create a new package
|
||||
*/
|
||||
createPackage: async (data: PackageCreate): Promise<Package> => {
|
||||
const response = await axiosClient.post<Package>('/packages/', data);
|
||||
const response = await axiosClient.post<Package>(PACKAGES_ENDPOINT, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -50,7 +52,7 @@ export const packageService = {
|
||||
* Update an existing package
|
||||
*/
|
||||
updatePackage: async (packageId: string, data: PackageUpdate): Promise<Package> => {
|
||||
const response = await axiosClient.put<Package>(`/packages/${packageId}`, data);
|
||||
const response = await axiosClient.put<Package>(`${PACKAGES_ENDPOINT}/${packageId}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -58,7 +60,9 @@ export const packageService = {
|
||||
* Delete a package
|
||||
*/
|
||||
deletePackage: async (packageId: string): Promise<{ message: string }> => {
|
||||
const response = await axiosClient.delete<{ message: string }>(`/packages/${packageId}`);
|
||||
const response = await axiosClient.delete<{ message: string }>(
|
||||
`${PACKAGES_ENDPOINT}/${packageId}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
PaginationParams,
|
||||
} from '@/types';
|
||||
|
||||
const PROJECTS_ENDPOINT = 'biz/api/v1/projects';
|
||||
const PROJECT_SUMMARY_ENDPOINT = 'biz/api/v1/dashboard/overview';
|
||||
|
||||
/**
|
||||
* Project Service
|
||||
*/
|
||||
@@ -17,7 +20,7 @@ export const projectService = {
|
||||
getProjects: async (params?: PaginationParams): Promise<PaginatedResponse<Project>> => {
|
||||
const skip = params?.skip ?? 0;
|
||||
const limit = params?.limit ?? 100;
|
||||
const response = await axiosClient.get<PaginatedResponse<Project>>(`/projects/`, {
|
||||
const response = await axiosClient.get<PaginatedResponse<Project>>(PROJECTS_ENDPOINT, {
|
||||
params: { skip, limit },
|
||||
});
|
||||
return response.data;
|
||||
@@ -27,7 +30,7 @@ export const projectService = {
|
||||
* Create a new project
|
||||
*/
|
||||
createProject: async (data: ProjectCreate): Promise<Project> => {
|
||||
const response = await axiosClient.post<Project>('/projects/', data);
|
||||
const response = await axiosClient.post<Project>(PROJECTS_ENDPOINT, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -35,7 +38,7 @@ export const projectService = {
|
||||
* Update an existing project
|
||||
*/
|
||||
updateProject: async (projectId: string, data: ProjectUpdate): Promise<Project> => {
|
||||
const response = await axiosClient.put<Project>(`/projects/${projectId}`, data);
|
||||
const response = await axiosClient.put<Project>(`${PROJECTS_ENDPOINT}/${projectId}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -43,7 +46,9 @@ export const projectService = {
|
||||
* Delete a project
|
||||
*/
|
||||
deleteProject: async (projectId: string): Promise<{ message: string }> => {
|
||||
const response = await axiosClient.delete<{ message: string }>(`/projects/${projectId}`);
|
||||
const response = await axiosClient.delete<{ message: string }>(
|
||||
`${PROJECTS_ENDPOINT}/${projectId}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -51,7 +56,9 @@ export const projectService = {
|
||||
* Fetch project summary (detections across packages and chainages)
|
||||
*/
|
||||
getProjectSummary: async (projectId: string): Promise<any> => {
|
||||
const response = await axiosClient.get(`/summary/projects/${projectId}`);
|
||||
const response = await axiosClient.get(PROJECT_SUMMARY_ENDPOINT, {
|
||||
params: { project_id: projectId },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -59,8 +66,8 @@ export const projectService = {
|
||||
* Fetch project summary filtered by video ID
|
||||
*/
|
||||
getProjectSummaryByVideo: async (projectId: string, videoId: string): Promise<any> => {
|
||||
const response = await axiosClient.get(`/summary/projects/${projectId}`, {
|
||||
params: { video_id: videoId },
|
||||
const response = await axiosClient.get(PROJECT_SUMMARY_ENDPOINT, {
|
||||
params: { project_id: projectId, video_id: videoId },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ENV_CONSTANT } from '@/constants/secrect.constant';
|
||||
import { tokenService } from '@/services/token.service';
|
||||
import { useAppStore } from '@/store/app.store';
|
||||
import { useAuthStore } from '@/store/auth.store';
|
||||
import axios from 'axios';
|
||||
import { signOut } from 'next-auth/react';
|
||||
|
||||
const BASE_URL = ENV_CONSTANT.BASE_API_URL;
|
||||
|
||||
@@ -14,8 +14,8 @@ const axiosClient = axios.create({
|
||||
});
|
||||
|
||||
axiosClient.interceptors.request.use(
|
||||
async (config) => {
|
||||
const token = await tokenService.getAccessToken();
|
||||
(config) => {
|
||||
const token = useAuthStore.getState().accessToken;
|
||||
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
@@ -31,8 +31,43 @@ axiosClient.interceptors.request.use(
|
||||
axiosClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
if (error?.response?.status === 401 && typeof window !== 'undefined') {
|
||||
await signOut({ callbackUrl: '/login' });
|
||||
const originalRequest = error?.config;
|
||||
const status = error?.response?.status;
|
||||
const requestUrl = originalRequest?.url ?? '';
|
||||
const isAuthRoute =
|
||||
requestUrl.includes('api/auth/login') || requestUrl.includes('api/auth/refresh');
|
||||
|
||||
if (status === 401 && originalRequest && !originalRequest._retry && !isAuthRoute) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
try {
|
||||
const response = await axiosAuth.post(
|
||||
'api/auth/refresh',
|
||||
{},
|
||||
{
|
||||
withCredentials: true,
|
||||
},
|
||||
);
|
||||
const accessToken = response.data?.access_token;
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error('Refresh failed: no access token');
|
||||
}
|
||||
|
||||
useAuthStore.getState().setAccessToken(accessToken);
|
||||
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||
|
||||
return axiosClient(originalRequest);
|
||||
} catch (refreshError) {
|
||||
useAuthStore.getState().logout();
|
||||
useAppStore.getState().clear();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
|
||||
16
src/services/initializer.service.ts
Normal file
16
src/services/initializer.service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { authService } from '@/services/api/auth.service';
|
||||
import { useAppStore } from '@/store/app.store';
|
||||
import type { UserProfile } from '@/types';
|
||||
|
||||
export async function initializeAuthenticatedApp() {
|
||||
const [{ user, tenant }, permissionsResponse] = await Promise.all([
|
||||
authService.me(),
|
||||
authService.permissions(),
|
||||
]);
|
||||
|
||||
useAppStore.getState().setUserContext({
|
||||
user: user as UserProfile,
|
||||
tenant,
|
||||
permissions: permissionsResponse.granted_permissions ?? [],
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { getSession } from 'next-auth/react';
|
||||
|
||||
export const tokenService = {
|
||||
getAccessToken: async (): Promise<string | null> => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
const session = await getSession();
|
||||
return session?.accessToken ?? null;
|
||||
},
|
||||
getRefreshToken: async (): Promise<string | null> => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
const session = await getSession();
|
||||
return session?.refreshToken ?? null;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user