From a7d1af91334f7e90bdb48e5e731ead7d1e304d19 Mon Sep 17 00:00:00 2001 From: MyPC Date: Tue, 23 Jun 2026 15:42:14 +0700 Subject: [PATCH 1/2] Migrasi autentikasi dari Bearer Token ke Sanctum Cookie-based --- frontend/.env.example | 3 +- frontend/src/api/axiosInstance.ts | 10 +- frontend/src/contexts/AuthContext.tsx | 93 +++++++--------- frontend/src/services/authService.ts | 15 ++- frontend/src/tests/pages/LoginPage.test.tsx | 34 +++--- .../src/tests/pages/RegisterPage.test.tsx | 9 +- frontend/src/types/auth.ts | 103 +++++++++--------- 7 files changed, 128 insertions(+), 139 deletions(-) diff --git a/frontend/.env.example b/frontend/.env.example index 6789bad..0ea2969 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1 +1,2 @@ -VITE_API_URL=http://localhost:8000/api \ No newline at end of file +VITE_API_URL=http://localhost:8000/api +VITE_API_BASE_URL=http://localhost:8000 \ No newline at end of file diff --git a/frontend/src/api/axiosInstance.ts b/frontend/src/api/axiosInstance.ts index d47a347..3587f73 100644 --- a/frontend/src/api/axiosInstance.ts +++ b/frontend/src/api/axiosInstance.ts @@ -9,19 +9,11 @@ const axiosInstance = axios.create({ withCredentials: true, }); -axiosInstance.interceptors.request.use((config) => { - const token = sessionStorage.getItem('__auth_token__'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); - axiosInstance.interceptors.response.use( (response) => response, (error: AxiosError) => { if (error.response?.status === 401) { - // Token expired / invalid — caller handles redirect + // Session expired — caller handles redirect } return Promise.reject(error); } diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 1cabd7b..8c7b23e 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,56 +1,39 @@ import { - createContext, - useState, - useEffect, - useCallback, - type ReactNode, - } from 'react'; - import type { AuthUser, AuthState, LoginResponseData } from '../types/auth'; - - interface AuthContextValue extends AuthState { - setAuth: (data: LoginResponseData) => void; - clearAuth: () => void; - } - - export const AuthContext = createContext(null); - - const TOKEN_KEY = '__auth_token__'; - - export function AuthProvider({ children }: { children: ReactNode }) { - const [user, setUser] = useState(null); - const [token, setToken] = useState( - () => sessionStorage.getItem(TOKEN_KEY) - ); - - useEffect(() => { - if (token) { - sessionStorage.setItem(TOKEN_KEY, token); - } else { - sessionStorage.removeItem(TOKEN_KEY); - } - }, [token]); - - const setAuth = useCallback((data: LoginResponseData) => { - setToken(data.token); - setUser(data.user); - }, []); - - const clearAuth = useCallback(() => { - setToken(null); - setUser(null); - }, []); - - return ( - - {children} - - ); - } \ No newline at end of file + createContext, + useState, + useCallback, + type ReactNode, +} from 'react'; +import type { AuthUser, AuthState, LoginResponseData } from '../types/auth'; + +interface AuthContextValue extends AuthState { + setAuth: (data: LoginResponseData) => void; + clearAuth: () => void; +} + +export const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + + const setAuth = useCallback((data: LoginResponseData) => { + setUser(data.user); + }, []); + + const clearAuth = useCallback(() => { + setUser(null); + }, []); + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts index d12badd..b746785 100644 --- a/frontend/src/services/authService.ts +++ b/frontend/src/services/authService.ts @@ -7,10 +7,17 @@ import type { ApiSuccessResponse, } from '../types/auth'; +const CSRF_COOKIE_URL = `${import.meta.env.VITE_API_BASE_URL}/sanctum/csrf-cookie`; + +async function initCsrf(): Promise { + await axiosInstance.get(CSRF_COOKIE_URL, { baseURL: '' }); +} + export const authService = { async register( payload: RegisterPayload ): Promise> { + await initCsrf(); const response = await axiosInstance.post>( '/auth/register', payload @@ -21,9 +28,11 @@ export const authService = { async login( payload: LoginPayload ): Promise> { - const response = await axiosInstance.post< -ApiSuccessResponse ->('/auth/login', payload); + await initCsrf(); + const response = await axiosInstance.post>( + '/auth/login', + payload + ); return response.data; }, diff --git a/frontend/src/tests/pages/LoginPage.test.tsx b/frontend/src/tests/pages/LoginPage.test.tsx index 9eec75a..b88f88e 100644 --- a/frontend/src/tests/pages/LoginPage.test.tsx +++ b/frontend/src/tests/pages/LoginPage.test.tsx @@ -18,15 +18,14 @@ const mockSetAuth = vi.fn<(data: LoginResponseData) => void>(); const mockClearAuth = vi.fn<() => void>(); const mockAuthContextValue = { - user: null, - token: null, - isAuthenticated: false, - setAuth: mockSetAuth, - clearAuth: mockClearAuth, - } satisfies AuthState & { - setAuth: (data: LoginResponseData) => void; - clearAuth: () => void; - }; + user: null, + isAuthenticated: false, + setAuth: mockSetAuth, + clearAuth: mockClearAuth, +} satisfies AuthState & { + setAuth: (data: LoginResponseData) => void; + clearAuth: () => void; +}; function renderLoginPage() { return render( @@ -39,10 +38,8 @@ function renderLoginPage() { } const mockLoginResponse: { message: string; data: LoginResponseData } = { - message: 'Login successful.', + message: 'Login berhasil.', data: { - token: 'test-token-123', - token_type: 'Bearer', user: { user_id: 10, email: 'ucok@example.com', @@ -108,7 +105,9 @@ describe('LoginPage', () => { await waitFor(() => { expect(mockSetAuth).toHaveBeenCalledWith(mockLoginResponse.data); - expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { replace: true }); + expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { + replace: true, + }); }); }); @@ -164,7 +163,9 @@ describe('LoginPage', () => { fireEvent.click(screen.getByRole('button', { name: 'Login' })); await waitFor(() => { - expect(screen.getByRole('button', { name: 'Logging in...' })).toBeDisabled(); + expect( + screen.getByRole('button', { name: 'Logging in...' }) + ).toBeDisabled(); }); }); @@ -172,7 +173,10 @@ describe('LoginPage', () => { render( diff --git a/frontend/src/tests/pages/RegisterPage.test.tsx b/frontend/src/tests/pages/RegisterPage.test.tsx index 1cc7961..59b614b 100644 --- a/frontend/src/tests/pages/RegisterPage.test.tsx +++ b/frontend/src/tests/pages/RegisterPage.test.tsx @@ -19,7 +19,6 @@ const mockClearAuth = vi.fn<() => void>(); const mockAuthContextValue = { user: null, - token: null, isAuthenticated: false, setAuth: mockSetAuth, clearAuth: mockClearAuth, @@ -128,7 +127,9 @@ describe('RegisterPage', () => { }); it('displays generic error on network failure', async () => { - vi.mocked(authService.register).mockRejectedValueOnce(new Error('Network Error')); + vi.mocked(authService.register).mockRejectedValueOnce( + new Error('Network Error') + ); renderRegisterPage(); fireEvent.click(screen.getByRole('button', { name: 'Register' })); @@ -149,7 +150,9 @@ describe('RegisterPage', () => { fireEvent.click(screen.getByRole('button', { name: 'Register' })); await waitFor(() => { - expect(screen.getByRole('button', { name: 'Register...' })).toBeDisabled(); + expect( + screen.getByRole('button', { name: 'Register...' }) + ).toBeDisabled(); }); }); }); \ No newline at end of file diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts index 8d356e8..57c2cc0 100644 --- a/frontend/src/types/auth.ts +++ b/frontend/src/types/auth.ts @@ -1,54 +1,51 @@ export interface PatientProfile { - patient_id: number; - name: string; - phone: string; - bpjs_number: string | null; - birth_place: string | null; - birth_date: string | null; - gender: string | null; - } - - export type UserRole = 'patient' | 'doctor' | 'nurse' | 'admin'; - - export interface AuthUser { - user_id: number; - email: string; - role: UserRole; - status: string; - profile: PatientProfile; - } - - export interface AuthState { - user: AuthUser | null; - token: string | null; - isAuthenticated: boolean; - } - - export interface RegisterPayload { - name: string; - email: string; - password: string; - password_confirmation: string; - // phone: string; - } - - export interface LoginPayload { - email: string; - password: string; - } - - export interface LoginResponseData { - token: string; - token_type: string; - user: AuthUser; - } - - export interface ApiSuccessResponse { - message: string; - data: T; - } - - export interface ApiValidationError { - message: string; - errors: Record; - } \ No newline at end of file + patient_id: number; + name: string; + phone: string; + bpjs_number: string | null; + birth_place: string | null; + birth_date: string | null; + gender: string | null; +} + +export type UserRole = 'patient' | 'doctor' | 'nurse' | 'admin'; + +export interface AuthUser { + user_id: number; + email: string; + role: UserRole; + status: string; + profile: PatientProfile; +} + +export interface AuthState { + user: AuthUser | null; + isAuthenticated: boolean; +} + +export interface RegisterPayload { + name: string; + email: string; + password: string; + password_confirmation: string; + phone: string; +} + +export interface LoginPayload { + email: string; + password: string; +} + +export interface LoginResponseData { + user: AuthUser; +} + +export interface ApiSuccessResponse { + message: string; + data: T; +} + +export interface ApiValidationError { + message: string; + errors: Record; +} \ No newline at end of file From c8b5d0b762e560c48da7bb56b7f2a9affc321f61 Mon Sep 17 00:00:00 2001 From: MyPC Date: Tue, 23 Jun 2026 15:48:33 +0700 Subject: [PATCH 2/2] Migrasi autentikasi dari Bearer Token ke Sanctum Cookie-based --- frontend/src/types/auth.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts index 57c2cc0..031f2d4 100644 --- a/frontend/src/types/auth.ts +++ b/frontend/src/types/auth.ts @@ -28,7 +28,6 @@ export interface RegisterPayload { email: string; password: string; password_confirmation: string; - phone: string; } export interface LoginPayload {