Skip to content

update pagination control and 2FA complete login flow #434

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "@coreui/coreui-free-react-admin-template",
"version": "5.2.0",
"description": "CoreUI Free React Admin Template",
"name": "EnglishDen school",
"version": "2.0.0",
"description": "EnglishDen School management software",
"homepage": ".",
"bugs": {
"url": "https://github.com/coreui/coreui-free-react-admin-template/issues"
},
"repository": {
"type": "git",
"url": "[email protected]:coreui/coreui-free-react-admin-template.git"
"url": "[email protected]:gpimthong/coreui.git"
},
"license": "MIT",
"author": "The CoreUI Team (https://github.com/orgs/coreui/people)",
"author": "Govit Pimthong",
"scripts": {
"build": "vite build",
"lint": "eslint \"src/**/*.js\"",
Expand All @@ -27,10 +27,12 @@
"@coreui/react-chartjs": "^3.0.0",
"@coreui/utils": "^2.0.2",
"@popperjs/core": "^2.11.8",
"axios": "^1.7.9",
"chart.js": "^4.4.6",
"classnames": "^2.5.1",
"core-js": "^3.39.0",
"prop-types": "^15.8.1",
"qrcode.react": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^9.1.2",
Expand Down
6 changes: 6 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import './scss/style.scss'

// We use those styles to show code examples, you should remove them in your application.
import './scss/examples.scss'
import TwoFactorVerification from './views/pages/login/TwoFactorVerification'
import TwoFactorSetupAndVerification from './views/pages/login/OTPVerify'
import Logout from './views/pages/logout/Logout'

// Containers
const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout'))
Expand Down Expand Up @@ -46,6 +49,9 @@ const App = () => {
>
<Routes>
<Route exact path="/login" name="Login Page" element={<Login />} />
<Route exact path="/logout" name="logout Page" element={<Logout />} />
<Route exact path="/2fa-verification" name="2FA verification page" element={<TwoFactorVerification />} />
<Route exact path="/2fa-setup-and-verification" name="2FA setup and verification page" element={<TwoFactorSetupAndVerification />} />
<Route exact path="/register" name="Register Page" element={<Register />} />
<Route exact path="/404" name="Page 404" element={<Page404 />} />
<Route exact path="/500" name="Page 500" element={<Page500 />} />
Expand Down
18 changes: 15 additions & 3 deletions src/_nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
cilPuzzle,
cilSpeedometer,
cilStar,
cilBank,
cilAccountLogout,
} from '@coreui/icons'
import { CNavGroup, CNavItem, CNavTitle } from '@coreui/react'

Expand All @@ -21,11 +23,21 @@ const _nav = [
component: CNavItem,
name: 'Dashboard',
to: '/dashboard',
icon: <CIcon icon={cilSpeedometer} customClassName="nav-icon" />,
badge: {
icon: <CIcon icon={cilBank} customClassName="nav-icon text-primary" />,
/* badge: {
color: 'info',
text: 'NEW',
},
}, */
},
{
component: CNavItem,
name: 'Logout',
to: '/logout',
icon: <CIcon icon={cilAccountLogout} customClassName="nav-icon text-warning" />,
/* badge: {
color: 'info',
text: 'NEW',
}, */
},
{
component: CNavTitle,
Expand Down
270 changes: 270 additions & 0 deletions src/api/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { useState, useEffect } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import { cibWindows } from '@coreui/icons';

const API_BASE_URL = 'http://10.10.7.83:8000/';



// Create an Axios instance
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});

// Function to get tokens from storage (localStorage/sessionStorage)
const getTokens = () => ({
access: localStorage.getItem('access_token'),
refresh: localStorage.getItem('refresh_token'),
});

// Function to save tokens to storage
const saveTokens = ({ access, refresh }) => {
localStorage.setItem('access_token', access);
localStorage.setItem('refresh_token', refresh);
};

// Function to remove tokens from storage
const clearTokens = () => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
};

// Add a request interceptor
api.interceptors.request.use(
(config) => {
const { access } = getTokens();
if (access) {
// If the access token is found, set it in the Authorization header
config.headers.Authorization = `Bearer ${access}`;
} else {
// If no access token is found, redirect to login
window.___location.href = '#/login';
}
return config;
},
(error) => Promise.reject(error)
);

// Add a response interceptor
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;

// Check if error is due to an expired access token
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; // Prevent retry loop
const { refresh } = getTokens();

if (refresh) {
try {
// Attempt to refresh the access token
const response = await axios.post(`${API_BASE_URL}api/token/refresh/`, { refresh });
const { access } = response.data;
saveTokens({ access, refresh }); // Update tokens in storage

// Retry the original request with the new access token
originalRequest.headers.Authorization = `Bearer ${access}`;
return api(originalRequest);
} catch (refreshError) {
clearTokens(); // Clear tokens if refresh fails
return Promise.reject(refreshError);
}
}
}

return Promise.reject(error);
}
);




const useFetchOne = (model, id) => {
const [data, setData] = useState(null); // Start with null for better clarity
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchData = async () => {
setLoading(true); // Ensure loading is reset if dependencies change
try {
const url = `api/v1/RestAPI/?model_name=${model}&id=${id}`;
const response = await api.get(url);

if (!response.ok) {
throw new Error(`Failed to fetch ${model} with ID ${id}: ${response.statusText}`);
}

const result = await response.json();
setData(result);
} catch (err) {
setError(err.message || 'An unexpected error occurred');
} finally {
setLoading(false);
}
};

fetchData();
}, [model, id]);

return { data, loading, error };
};

const useTableData = (model, skipKeys = [], patternsToModify = []) => {
const [tableData, setTableData] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchData = async () => {
try {


const default_page_size = 5;

const response = await api.get('api/v1/RestAPI/?model_name=' + model + '&page_size=' + default_page_size);

const data = response.data.results;

const total_items = response.data.count;
const total_pages = response.data.count;
const current_page = response.data.current_page;
const next_url = response.data.next; //can be null
const prev_url = response.data.previous; //can be null


//console.log(response.data);

const processedData = {
columns: Object.keys(data[0])
.filter((key) => !skipKeys.includes(key))
.map((key) => ({
key,
label: patternsToModify
.reduce((modifiedKey, { pattern, replacement }) => {
return modifiedKey.replace(pattern, replacement);
}, key)
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase()),
_props: { scope: 'col' },
})),
items: data.map((item) => {
const row = { _cellProps: { id: { scope: 'row' } } };
Object.keys(item)
.filter((key) => !skipKeys.includes(key))
.forEach((key) => {
let value = item[key];
const datePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}$/;
if (typeof value === 'string' && datePattern.test(value)) {
value = new Date(value).toLocaleString();
}
row[key] = value;
});
return row;
}),
};

setTableData(processedData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

fetchData();
}, []);

return { tableData, loading, error };
};





const useTablePaginatedData = (model, skipKeys = [], patternsToModify = [], defaultPageSize = 5) => {
const [tableData, setTableData] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [pagination, setPagination] = useState({
currentPage: 1,
pageSize: defaultPageSize,
totalItems: 0,
totalPages: 0,
});


useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await api.get(
`api/v1/RestAPI/?model_name=${model}&page_size=${pagination.pageSize}&page=${pagination.currentPage}`
);

const data = response.data.results;
//console.log(data);

setTableData({
columns: Object.keys(data[0])
.filter((key) => !skipKeys.includes(key))
.map((key) => ({
key,
label: patternsToModify
.reduce((modifiedKey, { pattern, replacement }) => {
return modifiedKey.replace(pattern, replacement);
}, key)
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase()),
_props: { scope: 'col' },
})),
items: data.map((item) => {
const row = { _cellProps: { id: { scope: 'row' } } };
Object.keys(item)
.filter((key) => !skipKeys.includes(key))
.forEach((key) => {
let value = item[key];
const datePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}$/;
if (typeof value === 'string' && datePattern.test(value)) {
value = new Date(value).toLocaleString();
}
row[key] = value;
});
return row;
}),
});

setPagination({
...pagination,
totalItems: response.data.count,
totalPages: Math.ceil(response.data.count / pagination.pageSize),
});
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

fetchData();
}, [model, pagination.currentPage, pagination.pageSize]);

return { tableData, loading, error, pagination, setPagination };
};


export {
useTableData,
useTablePaginatedData,
useFetchOne,
API_BASE_URL,
api,
saveTokens,
getTokens,
clearTokens,
}
2 changes: 1 addition & 1 deletion src/components/AppContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import routes from '../routes'

const AppContent = () => {
return (
<CContainer className="px-4" lg>
<CContainer className="px-1 px-md-2 px-lg-4" fluid>
<Suspense fallback={<CSpinner color="primary" />}>
<Routes>
{routes.map((route, idx) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/AppHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ const AppHeader = () => {
<AppHeaderDropdown />
</CHeaderNav>
</CContainer>
<CContainer className="px-4" fluid>
{/* <CContainer className="px-4" fluid>
<AppBreadcrumb />
</CContainer>
</CContainer> */}
</CHeader>
)
}
Expand Down
Loading