From 0548118208823c8ca22bf14bf07d7a7a6f28ded9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Sat, 16 Aug 2025 14:38:45 +0330 Subject: [PATCH] feat: refresh token added --- .env | 2 +- src/components/ProtectedRoute.tsx | 2 +- src/features/authorization/api/identityAPI.ts | 109 ++++++++++++++++++ .../AuthenticationSteps/EnterPasswordForm.tsx | 8 ++ src/lib/apiClient.ts | 9 +- 5 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 src/features/authorization/api/identityAPI.ts diff --git a/.env b/.env index b5ec710..b8e0f03 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/ -VITE_DEFUALT_AUTH_RETURN_URL=/setting/profile +VITE_DEFUALT_AUTH_RETURN_URL=/signup VITE_API_URL=https://accounts.business-harmony.com/api/ VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token VITE_IDENTITY_CLIENT_ID=harmony_identity diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index d2014c9..0550805 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,4 +1,4 @@ -import { getAccessToken } from '@/lib/apiClient'; +import { getAccessToken } from '@/features/authorization/api/identityAPI'; import { type PropsWithChildren } from 'react'; import { Navigate } from 'react-router-dom'; diff --git a/src/features/authorization/api/identityAPI.ts b/src/features/authorization/api/identityAPI.ts new file mode 100644 index 0000000..f06b7d8 --- /dev/null +++ b/src/features/authorization/api/identityAPI.ts @@ -0,0 +1,109 @@ +import apiClient from '@/lib/apiClient'; +import axios from 'axios'; + +export interface GenerateToken { + username: string; + password: string; +} + +export interface GenerateTokenResponse { + access_token: string; + expires_in: number; + token_type: 'Bearer'; + refresh_token: string; + scope: string; +} + +const ACCESS_TOKEN_KEY: 'access_token' = 'access_token' as const; +const REFRESH_TOKEN_KEY: 'refresh_token' = 'refresh_token' as const; +const EXPIRES_IN_KEY: 'expires_in' = 'expires_in' as const; + +export const generateToken = async (request: GenerateToken) => { + const body = new URLSearchParams(); + body.set('grant_type', 'password'); + body.set('client_id', import.meta.env.VITE_IDENTITY_CLIENT_ID); + body.set('scope', import.meta.env.VITE_IDENTITY_SCOPE); + body.set('username', request.username); + body.set('password', request.password); + + const result = await apiClient.post( + import.meta.env.VITE_IDENTITY_URL, + body.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); + + sessionStorage.setItem(ACCESS_TOKEN_KEY, result.data.access_token); + sessionStorage.setItem(REFRESH_TOKEN_KEY, result.data.refresh_token); + sessionStorage.setItem( + EXPIRES_IN_KEY, + String(Date.now() + result.data.expires_in * 1000), + ); +}; + +export const getAccessToken = () => { + const sessionToken = sessionStorage.getItem(ACCESS_TOKEN_KEY); + + if (sessionToken) return null; + + const expiresAt = Number(sessionStorage.getItem(EXPIRES_IN_KEY)); + const now = Date.now(); + + if (now < expiresAt) { + return sessionToken; + } else { + return refreshAccessToken(); + } +}; + +const refreshAccessToken = async () => { + const refreshToken = sessionStorage.getItem(REFRESH_TOKEN_KEY); + if (!refreshToken) return null; + + const result = await axios.post( + import.meta.env.VITE_IDENTITY_URL, + new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: 'harmony_identity', // from your token payload + }), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); + + if (result.data.access_token) { + sessionStorage.setItem(ACCESS_TOKEN_KEY, result.data.access_token); + sessionStorage.setItem( + EXPIRES_IN_KEY, + String(Date.now() + result.data.expires_in * 1000), + ); + return result.data.access_token; + } + + return null; +}; + +// export const generateToken = (request: any) => { +// const body = new URLSearchParams(); +// body.set('grant_type', request.password ? 'password' : 'otp'); +// body.set('client_id', import.meta.env.VITE_IDENTITY_CLIENT_ID); +// body.set('scope', import.meta.env.VITE_IDENTITY_SCOPE); +// if (request.phoneNumber) body.set('phonenumber', request.phoneNumber); +// if (request.email) body.set('email', request.email); +// if (request.password) body.set('password', request.password); +// if (request.otp) body.set('otp', request.otp); + +// return axios +// .post(import.meta.env.VITE_IDENTITY_URL, body.toString(), { +// headers: { +// 'Content-Type': 'application/x-www-form-urlencoded', +// }, +// }) +// .then((result) => console.log(result)); +// }; diff --git a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx index 47f4c95..59de5b2 100644 --- a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx @@ -21,6 +21,7 @@ import { import type { PasswordLoginRequest } from '../../types/userTypes'; import { Icon, useToast } from '@rkheftan/harmony-ui'; import { useApi } from '@/hooks/useApi'; +import { generateToken } from '../../api/identityAPI'; export interface EnterPasswordFormProps { onEditValue: () => void; @@ -79,6 +80,13 @@ export const EnterPasswordForm = ({ if (!loginWithPassResult) return; if (loginWithPassResult.success) { + console.log('logged in'); + + await generateToken({ + username: apiRequest.email ?? (apiRequest.phoneNumber as string), + password: apiRequest.password, + }); + onLoggedIn(loginWithPassResult.userId); toast({ diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts index d433f73..346a0b5 100644 --- a/src/lib/apiClient.ts +++ b/src/lib/apiClient.ts @@ -1,9 +1,6 @@ +import { getAccessToken } from '@/features/authorization/api/identityAPI'; import axios from 'axios'; -// Function to get the token from local storage or state management -export const ACCESS_TOKEN_KEY: 'access_token' = 'access_token' as const; -export const getAccessToken = () => sessionStorage.getItem(ACCESS_TOKEN_KEY); - const apiClient = axios.create({ // Define the base URL for all API requests baseURL: import.meta.env.VITE_API_URL, @@ -21,8 +18,8 @@ const apiClient = axios.create({ // --- Request Interceptor --- // This runs BEFORE each request is sent apiClient.interceptors.request.use( - (config) => { - const token = getAccessToken(); + async (config) => { + const token = await getAccessToken(); if (token) { // Add the authorization token to the headers config.headers.Authorization = `Bearer ${token}`;