Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a3235ee
feat: add Settings page with configurable JWT token
jescalada Jul 8, 2025
39321c3
feat: add Snackbar to Settings page
jescalada Jul 8, 2025
25e90ef
fix: refactor axios config loading in UI
jescalada Jul 8, 2025
efa4d5b
fix: withCredentials missing issue and JWT auth skipping
jescalada Jul 8, 2025
766aa35
fix: Settings view card responsiveness
jescalada Jul 9, 2025
4a0f1e4
chore: fix linter
jescalada Jul 9, 2025
2f216c3
fix: move pushes error message out of table and replace all with Dang…
jescalada Jul 10, 2025
334b6fe
fix: undefined error message on missing JWT configuration
jescalada Jul 10, 2025
6501893
Merge branch 'main' into JWT-UI-integration
jescalada Jul 10, 2025
e22eec5
chore: fix linter and broken tests
jescalada Jul 10, 2025
8d02adf
Merge branch 'JWT-UI-integration' of https://github.com/jescalada/git…
jescalada Jul 10, 2025
2b67635
Merge branch 'main' into JWT-UI-integration
jescalada Jul 21, 2025
88e79d4
fix: navbar styling issue
jescalada Jul 21, 2025
8131f12
fix: missing error display
jescalada Jul 21, 2025
1e66bfb
chore: fix linter
jescalada Jul 21, 2025
e9ee6b6
chore: add clsx and replace class name building
jescalada Aug 22, 2025
f7c5fdf
Merge branch 'main' into JWT-UI-integration
jescalada Aug 22, 2025
df0fbf4
chore: fix Code button styling issue
jescalada Aug 22, 2025
b5e5dda
chore: clean up axios calls
jescalada Aug 22, 2025
aa8ee6a
chore: fix dep vulnerability
jescalada Aug 22, 2025
9d29901
chore: refactor code button styling and add ClickAwayListener
jescalada Aug 22, 2025
551e237
Merge branch 'main' into JWT-UI-integration
jescalada Aug 22, 2025
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
2 changes: 0 additions & 2 deletions src/constants/languageColors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// src/constants/languageColors.ts

export const languageColors: Record<string, string> = {
'1C Enterprise': '#814CCC',
'2-Dimensional Array': '#38761D',
Expand Down
16 changes: 15 additions & 1 deletion src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import User from './ui/views/User/User';
import UserList from './ui/views/UserList/UserList';
import RepoDetails from './ui/views/RepoDetails/RepoDetails';
import RepoList from './ui/views/RepoList/RepoList';
import SettingsView from './ui/views/Settings/Settings';

import { RepoIcon } from '@primer/octicons-react';
import { Group, AccountCircle, Dashboard } from '@material-ui/icons';
import { Group, AccountCircle, Dashboard, Settings } from '@material-ui/icons';

import { Route } from './types/models';

const dashboardRoutes: Route[] = [
Expand Down Expand Up @@ -97,6 +99,18 @@ const dashboardRoutes: Route[] = [
layout: '/dashboard',
visible: false,
},
{
path: '/admin/settings',
name: 'Settings',
icon: Settings,
component: (props) =>
<RouteGuard
component={SettingsView}
fullRoutePath={`/dashboard/admin/settings`}
/>,
layout: '/dashboard',
visible: true,
},
];

export default dashboardRoutes;
12 changes: 6 additions & 6 deletions src/service/passport/jwtAuthHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ const jwtAuthHandler = (overrideConfig = null) => {
return next();
}

if (req.isAuthenticated()) {
return next();
}

const token = req.header("Authorization");
if (!token) {
return res.status(401).send("No token provided\n");
Expand All @@ -30,11 +26,15 @@ const jwtAuthHandler = (overrideConfig = null) => {
const audience = expectedAudience || clientID;

if (!authorityURL) {
return res.status(500).send("OIDC authority URL is not configured\n");
return res.status(500).send({
message: "JWT handler: authority URL is not configured\n"
});
}

if (!clientID) {
return res.status(500).send("OIDC client ID is not configured\n");
return res.status(500).send({
message: "JWT handler: client ID is not configured\n"
});
}

const tokenParts = token.split(" ");
Expand Down
24 changes: 10 additions & 14 deletions src/ui/components/Navbars/DashboardNavbarLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useNavigate } from 'react-router-dom';
import { AccountCircle } from '@material-ui/icons';
import { getUser } from '../../services/user';
import axios from 'axios';
import { getCookie } from '../../utils';
import { getAxiosConfig } from '../../services/auth';
import { UserData } from '../../../types/models';

const useStyles = makeStyles(styles);
Expand Down Expand Up @@ -51,21 +51,17 @@ const DashboardNavbarLinks: React.FC = () => {

const logout = async () => {
try {
const response = await axios.post(
await axios.post(
`${process.env.VITE_API_URI || 'http://localhost:3000'}/api/auth/logout`,
{},
{
withCredentials: true,
headers: {
'X-CSRF-TOKEN': getCookie('csrf'),
},
},
);

if (!response.data.isAuth && !response.data.user) {
setAuth(false);
navigate(0);
}
getAxiosConfig(),
)
.then((res) => {
if (!res.data.isAuth && !res.data.user) {
setAuth(false);
navigate(0);
}
});
} catch (error) {
console.error('Logout failed:', error);
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/Navbars/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const Header: React.FC<HeaderProps> = (props) => {
});

return (
<AppBar style={{ borderRadius: '0px', zIndex: 10 }} className={classes.appBar + appBarClasses}>
<AppBar style={{ borderRadius: '0px', zIndex: 10, backgroundColor: 'black', boxShadow: 'none' }} className={classes.appBar + appBarClasses}>
<Toolbar className={classes.container}>
<div className={classes.flex}>
{/* Here we create navbar brand, based on route name */}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/Typography/Danger.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const useStyles = makeStyles(styles);
export default function Danger(props) {
const classes = useStyles();
const { children } = props;
return <div className={classes.defaultFontStyle + ' ' + classes.dangerText}>{children}</div>;
return <div className={classes.primaryText + ' ' + classes.dangerText}>{children}</div>;
}

Danger.propTypes = {
Expand Down
33 changes: 33 additions & 0 deletions src/ui/services/auth.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getCookie } from '../utils';

const baseUrl = import.meta.env.VITE_API_URI
? `${import.meta.env.VITE_API_URI}`
: `${location.origin}`;
Expand All @@ -20,3 +22,34 @@ export const getUserInfo = async () => {
return null;
}
};

/**
* Gets the Axios config for the UI
* @return {Object} The Axios config
*/
export const getAxiosConfig = () => {
console.log('getAxiosConfig', getCookie('csrf'), localStorage.getItem('ui_jwt_token'));
const jwtToken = localStorage.getItem('ui_jwt_token');
return {
withCredentials: true,
headers: {
'X-CSRF-TOKEN': getCookie('csrf'),
Authorization: jwtToken ? `Bearer ${jwtToken}` : undefined,
},
};
};

/**
* Processes authentication errors and returns a user-friendly error message
* @param {Object} error - The error object
* @return {string} The error message
*/
export const processAuthError = (error) => {
let errorMessage = `Failed to authorize user: ${error.response.data.trim()}. `;
if (!localStorage.getItem('ui_jwt_token')) {
errorMessage += 'Set your JWT token in the settings page or disable JWT auth in your app configuration.'
} else {
errorMessage += 'Check your JWT token or disable JWT auth in your app configuration.'
}
return errorMessage;
};
18 changes: 7 additions & 11 deletions src/ui/services/git-push.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import axios from 'axios';
import { getCookie } from '../utils.tsx';
import { getAxiosConfig, processAuthError } from './auth.js';

const baseUrl = import.meta.env.VITE_API_URI
? `${import.meta.env.VITE_API_URI}/api/v1`
: `${location.origin}/api/v1`;

const config = {
withCredentials: true,
};

const getPush = async (id, setIsLoading, setData, setAuth, setIsError) => {
const url = `${baseUrl}/push/${id}`;
await axios(url, config)
await axios(url, getAxiosConfig())
.then((response) => {
const data = response.data;
data.diff = data.steps.find((x) => x.stepName === 'diff');
Expand Down Expand Up @@ -42,7 +38,7 @@ const getPushes = async (
url.search = new URLSearchParams(query);

setIsLoading(true);
await axios(url.toString(), { withCredentials: true })
await axios(url.toString(), getAxiosConfig())
.then((response) => {
const data = response.data;
setData(data);
Expand All @@ -51,7 +47,7 @@ const getPushes = async (
setIsError(true);
if (error.response && error.response.status === 401) {
setAuth(false);
setErrorMessage('Failed to authorize user. If JWT auth is enabled, please check your configuration or disable it.');
setErrorMessage(processAuthError(error));
} else {
setErrorMessage(`Error fetching pushes: ${error.response.data.message}`);
}
Expand All @@ -72,7 +68,7 @@ const authorisePush = async (id, setMessage, setUserAllowedToApprove, attestatio
attestation,
},
},
{ withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } },
getAxiosConfig(),
)
.catch((error) => {
if (error.response && error.response.status === 401) {
Expand All @@ -89,7 +85,7 @@ const rejectPush = async (id, setMessage, setUserAllowedToReject) => {
let errorMsg = '';
let isUserAllowedToReject = true;
await axios
.post(url, {}, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } })
.post(url, {}, getAxiosConfig())
.catch((error) => {
if (error.response && error.response.status === 401) {
errorMsg = 'You are not authorised to reject...';
Expand All @@ -103,7 +99,7 @@ const rejectPush = async (id, setMessage, setUserAllowedToReject) => {
const cancelPush = async (id, setAuth, setIsError) => {
const url = `${baseUrl}/push/${id}/cancel`;
await axios
.post(url, {}, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } })
.post(url, {}, getAxiosConfig())
.catch((error) => {
if (error.response && error.response.status === 401) {
setAuth(false);
Expand Down
24 changes: 10 additions & 14 deletions src/ui/services/repo.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import axios from 'axios';
import { getCookie } from '../utils.tsx';
import { getAxiosConfig, processAuthError } from './auth.js';

const baseUrl = import.meta.env.VITE_API_URI
? `${import.meta.env.VITE_API_URI}/api/v1`
: `${location.origin}/api/v1`;

const config = {
withCredentials: true,
};

const canAddUser = (repoName, user, action) => {
const url = new URL(`${baseUrl}/repo/${repoName}`);
return axios
.get(url.toString(), config)
.get(url.toString(), getAxiosConfig())
.then((response) => {
const data = response.data;
if (action === 'authorise') {
Expand Down Expand Up @@ -44,7 +40,7 @@ const getRepos = async (
const url = new URL(`${baseUrl}/repo`);
url.search = new URLSearchParams(query);
setIsLoading(true);
await axios(url.toString(), config)
await axios(url.toString(), getAxiosConfig())
.then((response) => {
const data = response.data;
setData(data);
Expand All @@ -53,9 +49,9 @@ const getRepos = async (
setIsError(true);
if (error.response && error.response.status === 401) {
setAuth(false);
setErrorMessage('Failed to authorize user. If JWT auth is enabled, please check your configuration or disable it.');
setErrorMessage(processAuthError(error));
} else {
setErrorMessage(`Error fetching repositories: ${error.response.data.message}`);
setErrorMessage(`Error fetching repos: ${error.response.data.message}`);
}
}).finally(() => {
setIsLoading(false);
Expand All @@ -65,7 +61,7 @@ const getRepos = async (
const getRepo = async (setIsLoading, setData, setAuth, setIsError, id) => {
const url = new URL(`${baseUrl}/repo/${id}`);
setIsLoading(true);
await axios(url.toString(), config)
await axios(url.toString(), getAxiosConfig())
.then((response) => {
const data = response.data;
setData(data);
Expand All @@ -84,7 +80,7 @@ const getRepo = async (setIsLoading, setData, setAuth, setIsError, id) => {
const addRepo = async (onClose, setError, data) => {
const url = new URL(`${baseUrl}/repo`);
axios
.post(url, data, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } })
.post(url, data, getAxiosConfig())
.then(() => {
onClose();
})
Expand All @@ -100,7 +96,7 @@ const addUser = async (repoName, user, action) => {
const url = new URL(`${baseUrl}/repo/${repoName}/user/${action}`);
const data = { username: user };
await axios
.patch(url, data, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } })
.patch(url, data, getAxiosConfig())
.catch((error) => {
console.log(error.response.data.message);
throw error;
Expand All @@ -115,7 +111,7 @@ const deleteUser = async (user, repoName, action) => {
const url = new URL(`${baseUrl}/repo/${repoName}/user/${action}/${user}`);

await axios
.delete(url, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } })
.delete(url, getAxiosConfig())
.catch((error) => {
console.log(error.response.data.message);
throw error;
Expand All @@ -126,7 +122,7 @@ const deleteRepo = async (repoName) => {
const url = new URL(`${baseUrl}/repo/${repoName}/delete`);

await axios
.delete(url, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } })
.delete(url, getAxiosConfig())
.catch((error) => {
console.log(error.response.data.message);
throw error;
Expand Down
25 changes: 6 additions & 19 deletions src/ui/services/user.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import axios, { AxiosError, AxiosResponse } from 'axios';
import { getCookie } from '../utils';
import { getAxiosConfig, processAuthError } from './auth';
import { UserData } from '../../types/models';

type SetStateCallback<T> = (value: T | ((prevValue: T) => T)) => void;

const baseUrl = process.env.VITE_API_URI || location.origin;
const config = {
withCredentials: true,
};

const getUser = async (
setIsLoading?: SetStateCallback<boolean>,
Expand All @@ -23,7 +20,7 @@ const getUser = async (
console.log(url);

try {
const response: AxiosResponse<UserData> = await axios(url, config);
const response: AxiosResponse<UserData> = await axios(url, getAxiosConfig());
const data = response.data;

setData?.(data);
Expand All @@ -43,7 +40,6 @@ const getUsers = async (
setIsLoading: SetStateCallback<boolean>,
setData: SetStateCallback<UserData[]>,
setAuth: SetStateCallback<boolean>,
setIsError: SetStateCallback<boolean>,
setErrorMessage: SetStateCallback<string>,
query: Record<string, string> = {},
): Promise<void> => {
Expand All @@ -53,17 +49,13 @@ const getUsers = async (
setIsLoading(true);

try {
const response: AxiosResponse<UserData[]> = await axios(url.toString(), {
withCredentials: true,
});
const response: AxiosResponse<UserData[]> = await axios(url.toString(), getAxiosConfig());
setData(response.data);
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
setAuth(false);
setErrorMessage(
'Failed to authorize user. If JWT auth is enabled, please check your configuration or disable it.',
);
setErrorMessage(processAuthError(error));
} else {
const msg = (error.response?.data as any)?.message ?? error.message;
setErrorMessage(`Error fetching users: ${msg}`);
Expand All @@ -81,10 +73,7 @@ const updateUser = async (data: UserData): Promise<void> => {
const url = new URL(`${baseUrl}/api/auth/gitAccount`);

try {
await axios.post(url.toString(), data, {
withCredentials: true,
headers: { 'X-CSRF-TOKEN': getCookie('csrf') },
});
await axios.post(url.toString(), data, getAxiosConfig());
} catch (error) {
const axiosError = error as AxiosError;
if (axiosError.response) {
Expand All @@ -103,9 +92,7 @@ const getUserLoggedIn = async (
const url = new URL(`${baseUrl}/api/auth/me`);

try {
const response: AxiosResponse<UserData> = await axios(url.toString(), {
withCredentials: true,
});
const response: AxiosResponse<UserData> = await axios(url.toString(), getAxiosConfig());
const data = response.data;
setIsLoading(false);
setIsAdmin(data.admin || false);
Expand Down
Loading
Loading