DONE: add customer pages, implement auth on teller page

This commit is contained in:
ISMAIL MASSERAN
2026-07-21 10:24:14 +08:00
parent 3189e3a1e3
commit a671f7ad5c
48 changed files with 5627 additions and 273 deletions
+63 -62
View File
@@ -22,24 +22,25 @@ import ManageDisplays from './pages/ManageDisplays/ManageDisplays';
import ManageUsers from './pages/UserManagingScreen/UserManagingScreen';
import ViewQueues from './pages/ViewBranchQueues/ViewBranchQueues';
import { ROLES } from './constants.js';
import { clearSession, getToken, getUserData } from './utils/session.js';
export default function App() {
const [user, setUser] = useState();
/*
Kada se logiramo, ako vec postoji token u localStorage, provjerimo da li je validan (nije istekao)
Ako je validan, ulogujemo usera, ako nije ocistimo storage od starih podataka
Kada se logiramo, ako vec postoji token u cookie-u, provjerimo da li je validan (nije istekao)
Ako je validan, ulogujemo usera, ako nije ocistimo session
*/
useEffect(() => {
const token = localStorage.getItem('token');
const token = getToken();
if (token) {
const url = `${ SERVER_URL }/api/v1/auth`;
const url = `${SERVER_URL}/api/v1/auth`;
fetchData(url, 'GET')
.then(({ data, success }) => {
.then(({ success }) => {
if (success) {
setUser(JSON.parse(localStorage.getItem('userData')));
setUser(getUserData());
} else {
localStorage.removeItem('token');
localStorage.removeItem('userData');
clearSession();
setUser(null);
}
});
}
@@ -47,152 +48,152 @@ export default function App() {
return (
<>
<UserContext.Provider value={ { user, setUser } }>
<UserContext.Provider value={{ user, setUser }}>
<Header />
<Routes>
<Route exact path="/:tenantCode/manage/displays" element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<ManageDisplays />
</AuthGuard> } />
</AuthGuard>} />
<Route exact path="/:tenantCode/manage/stations" element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<ManageStations />
</AuthGuard> } />
</AuthGuard>} />
<Route exact path="/:tenantCode/manage/groups" element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<ManageGroups />
</AuthGuard> } />
</AuthGuard>} />
<Route exact path="/:tenantCode/manage/branches" element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<ManageBranches />
</AuthGuard> } />
</AuthGuard>} />
<Route exact path="/:tenantCode/companydetails"
element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<CompanyInfoUpdate />
</AuthGuard>
}
element={
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<CompanyInfoUpdate />
</AuthGuard>
}
/>
<Route exact path="/:tenantCode/manage/services" element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<ManageServices />
</AuthGuard> }
</AuthGuard>}
/>
<Route exact path="/:tenantCode/manage/users" element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<ManageUsers />
</AuthGuard>
} />
<Route exact path="/login" element={ <LoginScreen /> } />
<Route exact path="/login" element={<LoginScreen />} />
<Route exact path="/:tenantCode/manage/admins" element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN] }>
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN]}>
<ManageAdmins />
</AuthGuard> } />
</AuthGuard>} />
<Route exact path="/profile" element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<AdminProfile />
</AuthGuard> } />
<Route exact path="/" element={ <LoginScreen /> } />
</AuthGuard>} />
<Route exact path="/" element={<LoginScreen />} />
<Route exact path="/loginauth"
element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<LoginAuth />
</AuthGuard>
}
element={
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<LoginAuth />
</AuthGuard>
}
/>
<Route exact path="/:tenantCode/queues" element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<ViewQueues />
</AuthGuard> } />
</AuthGuard>} />
<Route exact path="/:tenantCode/home" element={
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<AuthGuard roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<HomePage></HomePage>
<CanAccess roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
<CanAccess roles={[ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN]}>
<>
{ user && (
{user && (
<>
<HomePageCard
title="Manage displays"
backgroundColor="var(--dark-blue)"
buttonColor="var(--light-blue)"
url={ `/${ user.tenantCode }/manage/displays` }
url={`/${user.tenantCode}/manage/displays`}
/>
<HomePageCard
title="Manage groups"
backgroundColor="var(--light-blue)"
buttonColor="var(--dark-blue)"
url={ `/${ user.tenantCode }/manage/groups` }
url={`/${user.tenantCode}/manage/groups`}
/>
<HomePageCard
title="Manage branches"
backgroundColor="var(--dark-blue)"
buttonColor="var(--light-blue)"
url={ `/${ user.tenantCode }/manage/branches` }
url={`/${user.tenantCode}/manage/branches`}
/>
<HomePageCard
title="Manage services"
backgroundColor="var(--light-blue)"
buttonColor="var(--dark-blue)"
url={ `/${ user.tenantCode }/manage/services` }
url={`/${user.tenantCode}/manage/services`}
/>
<HomePageCard
title="Manage teller stations"
backgroundColor="var(--light-blue)"
buttonColor="var(--dark-blue)"
url={ `/${ user.tenantCode }/manage/stations` }
url={`/${user.tenantCode}/manage/stations`}
/>
<HomePageCard
title="Manage company details"
backgroundColor="var(--dark-blue)"
buttonColor="var(--light-blue)"
url={ `/${ user.tenantCode }/companydetails` }
url={`/${user.tenantCode}/companydetails`}
/>
<HomePageCard
title="View queues"
backgroundColor="var(--light-blue)"
buttonColor="var(--dark-blue)"
url={ `/${ user.tenantCode }/queues` }
url={`/${user.tenantCode}/queues`}
/>
</>
) }
)}
</>
</CanAccess>
<CanAccess roles={ [ROLES.ROLE_SUPER_ADMIN] }>
<CanAccess roles={[ROLES.ROLE_SUPER_ADMIN]}>
<>
{ user && (
{user && (
<>
<HomePageCard title="Manage administrators"
backgroundColor="var(--dark-blue)"
buttonColor="var(--light-blue)"
url={ `/${ user.tenantCode }/manage/admins` }></HomePageCard>
backgroundColor="var(--dark-blue)"
buttonColor="var(--light-blue)"
url={`/${user.tenantCode}/manage/admins`}></HomePageCard>
<HomePageCard
title="Manage users"
title="Manage Teller"
backgroundColor="var(--light-blue)"
buttonColor="var(--dark-blue)"
url={ `/${ user.tenantCode }/manage/users` }
url={`/${user.tenantCode}/manage/users`}
/>
</>
) }
)}
</>
</CanAccess>
<CanAccess roles={ [ROLES.ROLE_BRANCH_ADMIN] }>
{ user && (
<CanAccess roles={[ROLES.ROLE_BRANCH_ADMIN]}>
{user && (
<HomePageCard
title="Manage users"
backgroundColor="var(--dark-blue)"
buttonColor="var(--light-blue)"
url={ `/${ user.tenantCode }/manage/users` }
url={`/${user.tenantCode}/manage/users`}
/>
) }
)}
</CanAccess>
</AuthGuard>
} />
<Route path="*" element={ <NotFound /> } />
<Route path="*" element={<NotFound />} />
</Routes>
</UserContext.Provider>
</>
@@ -1,12 +1,12 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { getUserData } from '../../utils/session.js';
export default function AuthGuard({ children, roles }) {
const navigate = useNavigate();
useEffect(() => {
const storedUserData = localStorage.getItem('userData');
const user = storedUserData ? JSON.parse(storedUserData) : null;
const user = getUserData();
if (!user) {
navigate('/login');
@@ -23,7 +23,7 @@ export default function AuthGuard({ children, roles }) {
return (
<>
{ children }
{children}
</>
);
@@ -6,16 +6,16 @@ import profileImage from '../../../assets/profile-user.png'
import './Header.css';
import { UserContext } from '../../context/UserContext.jsx';
import { clearSession } from '../../utils/session.js';
export default function Header() {
const navigate = useNavigate();
const { user, setUser } = useContext(UserContext);
function handleLogout() {
localStorage.removeItem('userData');
localStorage.removeItem('token');
clearSession();
setUser(null);
navigate('/');
navigate('/login');
}
const path = window.location.pathname;
@@ -4,6 +4,7 @@ import { SERVER_URL } from "../../constants.js";
import { fetchData } from '../../fetching/Fetch.js';
import { UserContext } from '../../context/UserContext.jsx';
import { useNavigate } from "react-router-dom";
import { getUserData, setSession } from '../../utils/session.js';
function doSubmit(submittedValues) {
console.log(`Submitted: ${submittedValues.join("")}`);
@@ -126,10 +127,9 @@ export default function LoginAuth() {
dispatch({ type: "VERIFY" });
try {
const storedUserData = localStorage.getItem('userData');
const userData = storedUserData ? JSON.parse(storedUserData) : null;
const userData = getUserData();
if (!userData) {
throw new Error("Email not found in localStorage");
throw new Error("Email not found in session");
}
const url = `${ SERVER_URL }/api/v1/auth/tfa`;
@@ -139,7 +139,11 @@ export default function LoginAuth() {
});
if (success) {
localStorage.setItem('token', data.token);
setSession({
token: data.token,
userData: data.userData,
isTfa: true,
});
setUser(data.userData);
navigate(`/${ data.userData.tenantCode }/home`);
} else {
@@ -5,6 +5,7 @@ import "./LoginForm.css";
import LoginAuth from "../LoginAuth/LoginAuth";
import { Route, Routes, useNavigate, Link } from "react-router-dom";
import { SERVER_URL } from "../../constants.js";
import { setSession } from "../../utils/session.js";
const LoginForm = () => {
const [username, setUsername] = useState("");
@@ -28,7 +29,11 @@ const LoginForm = () => {
return;
}
const data = await response.json();
localStorage.setItem('userData', JSON.stringify(data));
setSession({
userData: data.userData ?? data,
token: data.token,
isTfa: !data.token,
});
navigate('/');
}
@@ -67,9 +72,12 @@ const LoginForm = () => {
const data = await response.json();
localStorage.setItem('userData', JSON.stringify(data));
if (response.ok) {
setSession({
userData: data.userData ?? data,
token: data.token,
isTfa: !data.token,
});
setIsSubmitted(true);
navigate('/');
} else if (response.status === 403) {
+5 -3
View File
@@ -1,10 +1,12 @@
/*
Koristiti ovu funkciju za fetchanje u buducnosti kad god je to moguce.
*/
import { getToken, setToken } from '../utils/session.js';
export async function fetchData(url, method, body) {
const headers = new Headers();
const token = localStorage.getItem('token');
const token = getToken();
if (token) {
headers.append('Authorization', `Bearer ${ token }`);
}
@@ -27,9 +29,9 @@ export async function fetchData(url, method, body) {
//na svaki ispravan rezultat treba da dobijemo novi token da refreshamo stari
const newToken = res.headers.get('Auth-Token');
if (newToken) {
localStorage.setItem('token', newToken);
setToken(newToken);
}
}
return { data: data, success: res.ok };
}
}
@@ -4,6 +4,7 @@ import 'bootstrap/dist/css/bootstrap.min.css';
import { SERVER_URL } from '../../constants.js';
import { UserContext } from '../../context/UserContext.jsx';
import { useNavigate, useParams } from "react-router-dom";
import { getToken } from '../../utils/session.js';
const styles = {
primaryButton: {
@@ -30,22 +31,22 @@ const AdminManageScreen = () => {
const [adminEmail, setAdminEmail] = useState('');
const [adminPassword, setAdminPassword] = useState('');
const [selectedAdminIndex, setSelectedAdminIndex] = useState(null);
const [token, setToken] = useState('');
const [emailError, setEmailError] = useState('');
const [passwordError, setPasswordError] = useState('');
useEffect(() => {
const storedToken = localStorage.getItem('token');
if (storedToken) {
setToken(storedToken);
if (getToken()) {
fetchAdmins();
}
}, []);
useEffect(() => {
if (token) {
fetchAdmins();
}
}, [token]);
const authHeaders = () => {
const token = getToken();
return {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
};
const fetchAdmins = async () => {
try {
@@ -54,10 +55,7 @@ const AdminManageScreen = () => {
});
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
headers: authHeaders(),
body: requestBody
});
@@ -87,10 +85,7 @@ const AdminManageScreen = () => {
try {
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
headers: authHeaders(),
body: JSON.stringify(requestBody)
});
@@ -122,10 +117,7 @@ const AdminManageScreen = () => {
};
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user/${admins[selectedAdminIndex].id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
headers: authHeaders(),
body: JSON.stringify(updatedAdmin),
});
if (response.ok) {
@@ -146,10 +138,7 @@ const AdminManageScreen = () => {
try {
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user/${userId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': token
}
headers: authHeaders(),
});
if (response.ok) {
const updatedAdmins = admins.filter(admin => admin.id !== userId);
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { fetchData } from '../../fetching/Fetch.js';
import { SERVER_URL } from '../../constants';
import { getIsTfa, getUserData, setIsTfa } from '../../utils/session.js';
import "./AdminProfile.css"
export default function AdminProfile(){
@@ -10,12 +11,9 @@ export default function AdminProfile(){
const [userData, setUserData] = useState('');
useEffect(() => {
const storedUserData = localStorage.getItem('userData');
setUserData(JSON.parse(storedUserData));
const storedIsTfa = localStorage.getItem('isTfa');
let isTfa = JSON.parse(storedIsTfa);
setUserData(getUserData());
const isTfa = getIsTfa();
setIsChecked(isTfa);
setIsQRCodeEnabled(isTfa);
}, []);
@@ -25,7 +23,7 @@ export default function AdminProfile(){
const { data, success } = await fetchData(url, 'PUT', {
isTfa: isChecked
});
localStorage.setItem('isTfa', isChecked);
setIsTfa(isChecked);
setIsQRCodeEnabled(isChecked);
if(!isChecked){
setQrCodeSrc('');
@@ -1,13 +1,22 @@
import React, { useState } from 'react';
import React, { useContext, useState } from 'react';
import validator from 'validator';
import './LoginScreen.css';
import { useNavigate } from 'react-router-dom';
import { SERVER_URL } from '../../constants.js';
import { ROLES, SERVER_URL } from '../../constants.js';
import { fetchData } from '../../fetching/Fetch.js';
import { UserContext } from '../../context/UserContext.jsx';
import { clearSession, setSession } from '../../utils/session.js';
const ADMIN_ROLES = [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN];
function hasAdminAccess(userData) {
const roles = Array.isArray(userData?.roles) ? userData.roles : [];
return roles.some((role) => ADMIN_ROLES.includes(role));
}
export default function LoginScreen() {
const navigate = useNavigate();
const { setUser } = useContext(UserContext);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
@@ -38,26 +47,42 @@ export default function LoginScreen() {
password: password
});
if (success) {
if (data.userData == undefined) {
localStorage.setItem('userData', JSON.stringify(data));
} else {
localStorage.setItem('userData', JSON.stringify(data.userData));
localStorage.setItem('token', data.token);
setUser(data.userData);
}
setIsSubmitted(true);
if (data.token) {
localStorage.setItem('isTfa', false);
navigate(`/${data.userData.tenantCode}/home`);
} else {
localStorage.setItem('isTfa', true);
navigate('/loginauth');
}
} else {
if (!success || !data) {
setError('Your credentials are incorrect.');
return;
}
const userData = data.userData ?? data;
const token = data.token;
if (!hasAdminAccess(userData)) {
clearSession();
setUser(null);
setError(
'This account is a regular user (ROLE_USER) and cannot access the admin app. Create an admin via Manage administrators.'
);
return;
}
if (!token) {
setSession({ userData, isTfa: true });
setUser(userData);
navigate('/loginauth', { replace: true });
return;
}
if (!userData?.tenantCode) {
setError('Login succeeded but tenant information is missing.');
return;
}
setSession({
userData,
token,
isTfa: false,
});
setUser(userData);
navigate(`/${userData.tenantCode}/home`, { replace: true });
} catch (error) {
console.error('Error:', error);
setError('An error occurred. Please try again.');
@@ -73,11 +98,6 @@ export default function LoginScreen() {
setPassword(event.target.value);
setError('');
};
/*
if (isSubmitted) {
//navigate('/loginAuth');
navigate('/companydetails');
}*/
return (
<div id="login-form">
@@ -92,9 +112,7 @@ export default function LoginScreen() {
value={username}
onChange={handleUsernameChange}
/>
{error && (error.includes('Username') || error.includes('Invalid')) &&
<p className="error">{error}</p>}
{error && (error.includes('credentials')) && <p className="error">{error}</p>}
{error && !error.includes('Password') && <p className="error">{error}</p>}
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
@@ -116,4 +134,4 @@ export default function LoginScreen() {
</form>
</div>
);
};
};
@@ -2,8 +2,8 @@ import React, { useState, useEffect } from 'react';
import { Button, Table, Modal, Form } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import { SERVER_URL } from '../../constants.js';
import { UserContext } from '../../context/UserContext.jsx';
import { useNavigate, useParams } from "react-router-dom";
import { useParams } from "react-router-dom";
import { getToken } from '../../utils/session.js';
const styles = {
primaryButton: {
@@ -29,34 +29,31 @@ const UserManageScreen = () => {
const [userEmail, setUserEmail] = useState('');
const [userPassword, setUserPassword] = useState('');
const [selectedUserIndex, setSelectedUserIndex] = useState(null);
const [token, setToken] = useState('');
const [emailError, setEmailError] = useState('');
const [passwordError, setPasswordError] = useState('');
useEffect(() => {
const storedToken = localStorage.getItem('token');
if (storedToken) {
setToken(storedToken);
if (getToken()) {
fetchUsers();
}
}, []);
useEffect(() => {
if (token) {
fetchUsers();
}
}, [token]);
const authHeaders = () => {
const token = getToken();
return {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
};
const fetchUsers = async () => {
try {
const requestBody = JSON.stringify({
roleName: 'ROLE_USER'
});
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}`, {
const response = await fetch(`${SERVER_URL}/api/v1/admin/${tenantCode}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
headers: authHeaders(),
body: requestBody
});
@@ -84,12 +81,9 @@ const UserManageScreen = () => {
};
try {
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user`, {
const response = await fetch(`${SERVER_URL}/api/v1/admin/${tenantCode}/user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
headers: authHeaders(),
body: JSON.stringify(requestBody)
});
@@ -119,12 +113,9 @@ const UserManageScreen = () => {
const updatedUser = {
email: userEmail
};
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user/${users[selectedUserIndex].id}`, {
const response = await fetch(`${SERVER_URL}/api/v1/admin/${tenantCode}/user/${users[selectedUserIndex].id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
headers: authHeaders(),
body: JSON.stringify(updatedUser),
});
if (response.ok) {
@@ -144,12 +135,9 @@ const UserManageScreen = () => {
const handleDeleteUser = async (userId) => {
try {
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user/${userId}`, {
const response = await fetch(`${SERVER_URL}/api/v1/admin/${tenantCode}/user/${userId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': token
}
headers: authHeaders(),
});
if (response.ok) {
const updatedUsers = users.filter(user => user.id !== userId);
@@ -171,28 +159,32 @@ const UserManageScreen = () => {
return (
<div className="text-center">
<h2>Manage Users</h2>
<h2>Manage Teller</h2>
<p className="text-muted mb-3">
These accounts have ROLE_USER and cannot log into the admin app.
Use Manage administrators to create admin logins.
</p>
<Button variant="primary" style={styles.primaryButton} className="mb-3" onClick={() => { setShowModal(true); setSelectedUserIndex(null); }}>Add User</Button>
<Table striped bordered hover>
<thead>
<tr>
<th>ID</th>
<th>Email</th>
<th>Actions</th>
</tr>
<tr>
<th>ID</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{users.map((user, index) => (
<tr key={index}>
<td>{user.id}</td>
<td>{user.email}</td>
<td>
<Button variant="info" style={styles.infoButton} onClick={() => handleEditClick(index)}>Edit</Button>{' '}
<Button variant="danger" onClick={() => handleDeleteUser(user.id)}>Delete</Button>
</td>
</tr>
))}
{users.map((user, index) => (
<tr key={index}>
<td>{user.id}</td>
<td>{user.email}</td>
<td>
<Button variant="info" style={styles.infoButton} onClick={() => handleEditClick(index)}>Edit</Button>{' '}
<Button variant="danger" onClick={() => handleDeleteUser(user.id)}>Delete</Button>
</td>
</tr>
))}
</tbody>
</Table>
+124
View File
@@ -0,0 +1,124 @@
const TOKEN_KEY = 'token';
const USER_DATA_KEY = 'userData';
const IS_TFA_KEY = 'isTfa';
/** Matches backend jwt.token-validity-time (PT30M). */
const SESSION_MAX_AGE_SECONDS = 30 * 60;
function getCookie(name) {
const prefix = `${encodeURIComponent(name)}=`;
const parts = document.cookie ? document.cookie.split('; ') : [];
for (const part of parts) {
if (part.startsWith(prefix)) {
return decodeURIComponent(part.slice(prefix.length));
}
}
return null;
}
function setCookie(name, value, maxAgeSeconds = SESSION_MAX_AGE_SECONDS) {
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
document.cookie = [
`${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
`Path=/`,
`Max-Age=${maxAgeSeconds}`,
`SameSite=Lax`,
secure,
].join('; ');
}
function removeCookie(name) {
document.cookie = `${encodeURIComponent(name)}=; Path=/; Max-Age=0; SameSite=Lax`;
}
function migrateFromLocalStorage(key) {
const legacy = localStorage.getItem(key);
if (legacy == null) {
return null;
}
setCookie(key, legacy);
localStorage.removeItem(key);
return legacy;
}
export function getToken() {
return getCookie(TOKEN_KEY) ?? migrateFromLocalStorage(TOKEN_KEY);
}
export function setToken(token) {
if (token == null || token === '') {
removeCookie(TOKEN_KEY);
localStorage.removeItem(TOKEN_KEY);
return;
}
setCookie(TOKEN_KEY, token);
localStorage.removeItem(TOKEN_KEY);
}
export function getUserData() {
const raw = getCookie(USER_DATA_KEY) ?? migrateFromLocalStorage(USER_DATA_KEY);
if (!raw) {
return null;
}
try {
return JSON.parse(raw);
} catch {
removeCookie(USER_DATA_KEY);
return null;
}
}
export function setUserData(userData) {
if (userData == null) {
removeCookie(USER_DATA_KEY);
localStorage.removeItem(USER_DATA_KEY);
return;
}
setCookie(USER_DATA_KEY, JSON.stringify(userData));
localStorage.removeItem(USER_DATA_KEY);
}
export function getIsTfa() {
const raw = getCookie(IS_TFA_KEY) ?? migrateFromLocalStorage(IS_TFA_KEY);
if (raw == null) {
return false;
}
try {
return JSON.parse(raw);
} catch {
return raw === 'true';
}
}
export function setIsTfa(isTfa) {
setCookie(IS_TFA_KEY, JSON.stringify(Boolean(isTfa)));
localStorage.removeItem(IS_TFA_KEY);
}
export function setSession({ token, userData, isTfa } = {}) {
if (token !== undefined) {
setToken(token);
}
if (userData !== undefined) {
setUserData(userData);
}
if (isTfa !== undefined) {
setIsTfa(isTfa);
}
}
export function clearSession() {
removeCookie(TOKEN_KEY);
removeCookie(USER_DATA_KEY);
removeCookie(IS_TFA_KEY);
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_DATA_KEY);
localStorage.removeItem(IS_TFA_KEY);
}