feat: implement auth flow and app initialization

This commit is contained in:
2026-06-15 18:41:27 +05:30
parent 9de65d0ce6
commit 8d4ff49633
29 changed files with 551 additions and 535 deletions

View File

@@ -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;
},
};

View File

@@ -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;
},
};

View File

@@ -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;

View File

@@ -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;
},
};

View File

@@ -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;
},

View File

@@ -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);

View 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 ?? [],
});
}

View File

@@ -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;
},
};