Skip to content

fix: decouple UI layout from admin routes #961

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 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f2c1182
feat(auth): add NotAuthorized and NotFound pages
jescalada Feb 4, 2025
256523f
feat(auth): add AuthProvider and PrivateRoute wrapper
jescalada Feb 4, 2025
516ed7f
chore(auth): rename userLoggedIn endpoint
jescalada Feb 4, 2025
47e95d4
chore(auth): refactor routes to use PrivateRoute guard
jescalada Feb 4, 2025
893e0b1
fix(auth): temporary fix for username edit redirect
jescalada Feb 7, 2025
d049463
fix(auth): access user admin status correctly
jescalada Feb 7, 2025
db04af6
chore(auth): refactor /admin routes into /dashboard
jescalada Feb 11, 2025
9736801
chore(auth): rename Admin files into Dashboard
jescalada Feb 11, 2025
e7ec1f0
test(auth): Add default login redirect E2E test
jescalada Feb 11, 2025
3702acd
fix(auth): fix redirect on local login
jescalada Feb 11, 2025
f2560ab
fix(auth): improve error handling on repos page
jescalada Feb 11, 2025
24ca06f
test(auth): fix failing test (login before accessing page)
jescalada Feb 11, 2025
2146bc0
test(auth): fix login command and simplify login flow
jescalada Feb 11, 2025
6b96db0
Merge branch 'oidc-implementation' into layout-auth-decoupling
jescalada Mar 10, 2025
050ba17
chore(auth): fix linter issues
jescalada Mar 10, 2025
fd74b32
Merge branch 'main' into layout-auth-decoupling
jescalada Apr 2, 2025
4c4f513
fix(auth): fix issue during merge
jescalada Apr 2, 2025
714d068
Merge remote-tracking branch 'origin/main' into layout-auth-decoupling
jescalada May 1, 2025
86784f0
test: fix failing test (login required to view pushes)
jescalada May 1, 2025
d1c6c26
feat: add uiRouteAuth setting to config
jescalada May 2, 2025
645a27b
feat: add dynamic auth processing for PrivateRoute
jescalada May 2, 2025
351d67d
chore: rename PrivateRoute to RouteGuard
jescalada May 2, 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
21 changes: 21 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@
"cert": { "type": "string" }
},
"required": ["enabled", "key", "cert"]
},
"uiRouteAuth": {
"description": "UI routes that require authentication (logged in or admin)",
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"rules": {
"type": "array",
"items": {
"$ref": "#/definitions/routeAuthRule"
}
}
}
}
},
"definitions": {
Expand Down Expand Up @@ -119,6 +132,14 @@
"options": { "type": "object" }
},
"required": ["type", "enabled"]
},
"routeAuthRule": {
"type": "object",
"properties": {
"pattern": { "type": "string" },
"adminOnly": { "type": "boolean" },
"loginRequired": { "type": "boolean" }
}
}
},
"additionalProperties": false
Expand Down
4 changes: 3 additions & 1 deletion cypress/e2e/autoApproved.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import moment from 'moment';

describe('Auto-Approved Push Test', () => {
beforeEach(() => {
cy.login('admin', 'admin');

cy.intercept('GET', '/api/v1/push/123', {
statusCode: 200,
body: {
Expand Down Expand Up @@ -45,7 +47,7 @@ describe('Auto-Approved Push Test', () => {
});

it('should display auto-approved message and verify tooltip contains the expected timestamp', () => {
cy.visit('/admin/push/123');
cy.visit('/dashboard/push/123');

cy.wait('@getPush');

Expand Down
12 changes: 12 additions & 0 deletions cypress/e2e/login.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ describe('Login page', () => {
cy.get('[data-test="login"]').should('exist');
});

it('should redirect to repo list on valid login', () => {
cy.intercept('GET', '**/api/auth/me').as('getUser');

cy.get('[data-test="username"]').type('admin');
cy.get('[data-test="password"]').type('admin');
cy.get('[data-test="login"]').click();

cy.wait('@getUser');

cy.url().should('include', '/dashboard/repo');
})

describe('OIDC login button', () => {
it('should exist', () => {
cy.get('[data-test="oidc-login"]').should('exist');
Expand Down
6 changes: 4 additions & 2 deletions cypress/e2e/repo.cy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
describe('Repo', () => {
beforeEach(() => {
cy.visit('/admin/repo');
cy.login('admin', 'admin');

cy.visit('/dashboard/repo');

// prevent failures on 404 request and uncaught promises
cy.on('uncaught:exception', () => false);
Expand All @@ -18,7 +20,7 @@ describe('Repo', () => {

cy
// find the entry for finos/test-repo
.get('a[href="/admin/repo/test-repo"]')
.get('a[href="/dashboard/repo/test-repo"]')
// take it's parent row
.closest('tr')
// find the nearby span containing Code we can click to open the tooltip
Expand Down
6 changes: 5 additions & 1 deletion cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@
Cypress.Commands.add('login', (username, password) => {
cy.session([username, password], () => {
cy.visit('/login');
cy.intercept('GET', '**/api/auth/me').as('getUser');

cy.get('[data-test=username]').type(username);
cy.get('[data-test=password]').type(password);
cy.get('[data-test=login]').click();
cy.url().should('contain', '/admin/profile');

cy.wait('@getUser');
cy.url().should('include', '/dashboard/repo');
});
});
15 changes: 15 additions & 0 deletions proxy.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,20 @@
"enabled": true,
"key": "certs/key.pem",
"cert": "certs/cert.pem"
},
"uiRouteAuth": {
"enabled": false,
"rules": [
{
"pattern": "/dashboard/*",
"adminOnly": false,
"loginRequired": true
},
{
"pattern": "/admin/*",
"adminOnly": true,
"loginRequired": true
}
]
}
}
8 changes: 8 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
let _tlsEnabled = defaultSettings.tls.enabled;
let _tlsKeyPemPath = defaultSettings.tls.key;
let _tlsCertPemPath = defaultSettings.tls.cert;
let _uiRouteAuth: Record<string, unknown> = defaultSettings.uiRouteAuth;

// Get configured proxy URL
export const getProxyUrl = () => {
Expand Down Expand Up @@ -217,3 +218,10 @@
}
return _domains;
};

export const getUIRouteAuth = () => {
if (_userSettings && _userSettings.uiRouteAuth) {
_uiRouteAuth = _userSettings.uiRouteAuth;

Check warning on line 224 in src/config/index.ts

View check run for this annotation

Codecov / codecov/patch

src/config/index.ts#L224

Added line #L224 was not covered by tests
}
return _uiRouteAuth;

Check warning on line 226 in src/config/index.ts

View check run for this annotation

Codecov / codecov/patch

src/config/index.ts#L226

Added line #L226 was not covered by tests
};
23 changes: 15 additions & 8 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,28 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { createBrowserHistory } from 'history';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import { AuthProvider } from './ui/auth/AuthProvider';

// core components
import Admin from './ui/layouts/Admin';
import Dashboard from './ui/layouts/Dashboard';
import Login from './ui/views/Login/Login';
import './ui/assets/css/material-dashboard-react.css';
import NotAuthorized from './ui/views/Extras/NotAuthorized';
import NotFound from './ui/views/Extras/NotFound';

const hist = createBrowserHistory();

ReactDOM.render(
<Router history={hist}>
<Routes>
<Route exact path='/admin/*' element={<Admin />} />
<Route exact path='/login' element={<Login />} />
<Route exact path='/' element={<Navigate from='/' to='/admin/repo' />} />
</Routes>
</Router>,
<AuthProvider>
<Router history={hist}>
<Routes>
<Route exact path='/dashboard/*' element={<Dashboard />} />
<Route exact path='/login' element={<Login />} />
<Route exact path='/not-authorized' element={<NotAuthorized />} />
<Route exact path='/' element={<Navigate from='/' to='/dashboard/repo' />} />
<Route path='*' element={<NotFound />} />
</Routes>
</Router>
</AuthProvider>,
document.getElementById('root'),
);
2 changes: 1 addition & 1 deletion src/proxy/processors/push-action/blockForAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const exec = async (req: any, action: Action) => {
'\n\n\n' +
`\x1B[32mGitProxy has received your push ✅\x1B[0m\n\n` +
'🔗 Shareable Link\n\n' +
`\x1B[34m${url}/admin/push/${action.id}\x1B[0m` +
`\x1B[34m${url}/dashboard/push/${action.id}\x1B[0m` +
'\n\n\n';
step.setAsyncBlock(message);

Expand Down
82 changes: 56 additions & 26 deletions src/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

*/

import React from 'react';
import RouteGuard from './ui/components/RouteGuard/RouteGuard';
import Person from '@material-ui/icons/Person';
import OpenPushRequests from './ui/views/OpenPushRequests/OpenPushRequests';
import PushDetails from './ui/views/PushDetails/PushDetails';
Expand All @@ -33,58 +35,86 @@ const dashboardRoutes = [
path: '/repo',
name: 'Repositories',
icon: RepoIcon,
component: RepoList,
layout: '/admin',
component: (props) =>
<RouteGuard
component={RepoList}
fullRoutePath={`/dashboard/repo`}
/>,
layout: '/dashboard',
visible: true,
},
{
path: '/repo/:id',
name: 'Repo Details',
icon: Person,
component: (props) =>
<RouteGuard
component={RepoDetails}
fullRoutePath={`/dashboard/repo/:id`}
/>,
layout: '/dashboard',
visible: false,
},
{
path: '/push',
name: 'Dashboard',
icon: Dashboard,
component: OpenPushRequests,
layout: '/admin',
component: (props) =>
<RouteGuard
component={OpenPushRequests}
fullRoutePath={`/dashboard/push`}
/>,
layout: '/dashboard',
visible: true,
},
{
path: '/push/:id',
name: 'Open Push Requests',
icon: Person,
component: PushDetails,
layout: '/admin',
component: (props) =>
<RouteGuard
component={PushDetails}
fullRoutePath={`/dashboard/push/:id`}
/>,
layout: '/dashboard',
visible: false,
},
{
path: '/profile',
name: 'My Account',
icon: AccountCircle,
component: User,
layout: '/admin',
component: (props) =>
<RouteGuard
component={User}
fullRoutePath={`/dashboard/profile`}
/>,
layout: '/dashboard',
visible: true,
},
{
path: '/user/:id',
name: 'User',
icon: Person,
component: User,
layout: '/admin',
visible: false,
path: '/admin/user',
name: 'Users',
icon: Group,
component: (props) =>
<RouteGuard
component={UserList}
fullRoutePath={`/dashboard/admin/user`}
/>,
layout: '/dashboard',
visible: true,
},
{
path: '/repo/:id',
name: 'Repo Details',
path: '/admin/user/:id',
name: 'User',
icon: Person,
component: RepoDetails,
layout: '/admin',
component: (props) =>
<RouteGuard
component={User}
fullRoutePath={`/dashboard/admin/user/:id`}
/>,
layout: '/dashboard',
visible: false,
},
{
path: '/user',
name: 'Users',
icon: Group,
component: UserList,
layout: '/admin',
visible: true,
},
];

export default dashboardRoutes;
4 changes: 2 additions & 2 deletions src/service/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
return res.status(401).end();
}
console.log('Logged in successfully. User:', user);
return res.redirect(`${uiHost}:${uiPort}/admin/profile`);
return res.redirect(`${uiHost}:${uiPort}/dashboard/profile`);

Check warning on line 63 in src/service/routes/auth.js

View check run for this annotation

Codecov / codecov/patch

src/service/routes/auth.js#L63

Added line #L63 was not covered by tests
});
})(req, res, next);
});
Expand Down Expand Up @@ -135,7 +135,7 @@
}
});

router.get('/userLoggedIn', async (req, res) => {
router.get('/me', async (req, res) => {
if (req.user) {
const user = JSON.parse(JSON.stringify(req.user));
if (user && user.password) delete user.password;
Expand Down
4 changes: 4 additions & 0 deletions src/service/routes/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
res.send(config.getContactEmail());
});

router.get('/uiRouteAuth', function ({ res }) {
res.send(config.getUIRouteAuth());

Check warning on line 19 in src/service/routes/config.js

View check run for this annotation

Codecov / codecov/patch

src/service/routes/config.js#L19

Added line #L19 was not covered by tests
});

module.exports = router;
49 changes: 49 additions & 0 deletions src/ui/auth/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { getUserInfo } from '../services/auth';

// Interface for when we convert to TypeScript
// interface AuthContextType {
// user: any;
// setUser: (user: any) => void;
// refreshUser: () => Promise<void>;
// isLoading: boolean;
// }

const AuthContext = createContext(undefined);

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);

const refreshUser = async () => {
console.log('Refreshing user');
try {
const data = await getUserInfo();
setUser(data);
console.log('User refreshed:', data);
} catch (error) {
console.error('Error refreshing user:', error);
setUser(null);
} finally {
setIsLoading(false);
}
};

useEffect(() => {
refreshUser();
}, []);

return (
<AuthContext.Provider value={{ user, setUser, refreshUser, isLoading }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
Loading
Loading