Skip to content

feat: JWT apiAuthentication UI integration #1096

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
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
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