-
Notifications
You must be signed in to change notification settings - Fork 37
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
Custom authentication support #15
Comments
@KCFindstr were you able to solve this one? |
I tried to get around this by creating my own adminjs loader:
import { Injectable } from '@nestjs/common';
import { AbstractHttpAdapter } from '@nestjs/core';
import { AdminModuleOptions, AbstractLoader } from '@adminjs/nestjs';
import AdminJS from 'adminjs';
import adminJsExpressjs from '@adminjs/express';
// This file is modified from https://github.com/SoftwareBrothers/adminjs-nestjs/blob/fcd978e8db80b69766d3736b231e89be0f800d86/src/loaders/express.loader.ts
@Injectable()
export class AdminLoader implements AbstractLoader {
public register(
admin: AdminJS,
httpAdapter: AbstractHttpAdapter,
options: AdminModuleOptions,
) {
const app = httpAdapter.getInstance();
const router = adminJsExpressjs.buildRouter(
admin,
undefined,
options.formidableOptions,
);
// This named function is there on purpose.
// It names layer in main router with the name of the function, which helps localize
// admin layer in reorderRoutes() step.
app.use(options.adminJsOptions.rootPath, function admin(req, res, next) {
const session: any = req.session;
if (!session.adminUser) {
return res.redirect(options.adminJsOptions.loginPath);
}
return router(req, res, next);
});
this.reorderRoutes(app);
}
private reorderRoutes(app) {
let jsonParser;
let urlencodedParser;
let admin;
// Nestjs uses bodyParser under the hood which is in conflict with adminjs setup.
// Due to adminjs-expressjs usage of formidable we have to move body parser in layer tree after adminjs init.
// Notice! This is not documented feature of express, so this may change in the future. We have to keep an eye on it.
if (app && app._router && app._router.stack) {
const jsonParserIndex = app._router.stack.findIndex(
(layer: { name: string }) => layer.name === 'jsonParser',
);
if (jsonParserIndex >= 0) {
jsonParser = app._router.stack.splice(jsonParserIndex, 1);
}
const urlencodedParserIndex = app._router.stack.findIndex(
(layer: { name: string }) => layer.name === 'urlencodedParser',
);
if (urlencodedParserIndex >= 0) {
urlencodedParser = app._router.stack.splice(urlencodedParserIndex, 1);
}
const adminIndex = app._router.stack.findIndex(
(layer: { name: string }) => layer.name === 'admin',
);
if (adminIndex >= 0) {
admin = app._router.stack.splice(adminIndex, 1);
}
// if adminjs-nestjs didn't reorder the middleware
// the body parser would have come after corsMiddleware
const corsIndex = app._router.stack.findIndex(
(layer: { name: string }) => layer.name === 'corsMiddleware',
);
// in other case if there is no corsIndex we go after expressInit, because right after that
// there are nest endpoints.
const expressInitIndex = app._router.stack.findIndex(
(layer: { name: string }) => layer.name === 'expressInit',
);
const initIndex = (corsIndex >= 0 ? corsIndex : expressInitIndex) + 1;
app._router.stack.splice(
initIndex,
0,
...admin,
...jsonParser,
...urlencodedParser,
);
}
}
} I wrote this a while ago so I'm not sure if it still works with current version of adminjs, and I have no idea if there's official custom authentication support, either. |
great @KCFindstr! thanks for the solution, I actually had to do the same. I basically overwrote the routing logic of AdminJS and used for anyone who wants to do the same later:
function buildAuthenticatedRouter(admin: AdminJS, predefinedRouter?: express.Router | null, formidableOptions?: FormidableOptions): Router here's how mine looks like atm: export const buildAuthenticatedRouter = (
admin: AdminJS,
predefinedRouter?: express.Router | null,
formidableOptions?: FormidableOptions,
): Router => {
const router = predefinedRouter || express.Router();
router.use((req, _, next) => {
if ((req as any)._body) {
next(new OldBodyParserUsedError());
}
next();
});
router.use(formidableMiddleware(formidableOptions));
withProtectedAdminRoutesHandler(router, admin);
withLogin(router, admin); // <-- this function is what we need
withLogout(router, admin);
return buildRouter(admin, router, formidableOptions);
};
export const withLogin = (router: Router, admin: AdminJS): void => {
const { rootPath } = admin.options;
const loginPath = getLoginPath(admin);
const callbackPath = `${config.admin.path}/${loginPath}/callback`;
const authPath = `${config.admin.path}/${loginPath}/auth`;
passport.use(
new OAuth2Strategy(
{
// ...configs that you need
},
function (
accessToken: string,
refreshToken: string,
profile: any,
cb: (...args: any[]) => any,
) {
// you probably want to check some stuff here.
const decoded: any = jwt.decode(accessToken);
const userSession: CurrentAdmin = {
title: decoded["name"],
email: decoded["email"],
id: decoded["sid"],
avatarUrl:
decoded["profile"] ||
`https://ui-avatars.com/api/?name=${(decoded["name"] as string).replace(
" ",
"+",
)}`,
};
return cb(null, userSession);
},
),
);
// this route will only render the login page you have. take note that this must be overriden,
// as most probably you don't want to directly get the user's username/pass.
router.get(loginPath, async (_, res) => {
const login = await renderLogin(admin, {
action: authPath,
});
res.send(login);
});
router.get(path.join(loginPath, "auth"), passport.authenticate("oauth2"));
router.get(
path.join(loginPath, "callback"),
passport.authenticate("oauth2", { failureRedirect: `${config.admin.path}/login` }),
(req, res, next) => {
(req.session as any).adminUser = (req.session as any).passport.user;
req.session.save((err) => {
if (err) {
next(err);
}
if ((req.session as any).redirectTo) {
res.redirect(302, (req.session as any).redirectTo);
} else {
res.redirect(302, rootPath);
}
});
},
);
};
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { combineStyles } from "@adminjs/design-system";
import i18n from "i18next";
import React from "react";
import { renderToString } from "react-dom/server";
import { I18nextProvider } from "react-i18next";
import { Provider } from "react-redux";
import { Store } from "redux";
import { ServerStyleSheet, StyleSheetManager, ThemeProvider } from "styled-components";
import AdminJS, {
createStore,
getAssets,
getBranding,
getFaviconFromBranding,
initializeAssets,
initializeBranding,
initializeLocale,
ReduxState,
ViewHelpers,
} from "adminjs";
import LoginComponent from "./login-component";
type LoginTemplateAttributes = {
/**
* action which should be called when user clicks submit button
*/
action: string;
/**
* Error message to present in the form
*/
errorMessage?: string;
};
const html = async (admin: AdminJS, { action, errorMessage }: LoginTemplateAttributes): Promise<string> => {
const h = new ViewHelpers({ options: admin.options });
const store: Store<ReduxState> = createStore();
const branding = await getBranding(admin);
const assets = await getAssets(admin);
const faviconTag = getFaviconFromBranding(branding);
const scripts = ((assets && assets.scripts) || []).map((s) => `<script src="${s}"></script>`);
const styles = ((assets && assets.styles) || []).map(
(l) => `<link rel="stylesheet" type="text/css" href="${l}">`,
);
store.dispatch(initializeBranding(branding));
store.dispatch(initializeAssets(assets));
store.dispatch(initializeLocale(admin.locale));
const theme = combineStyles((branding && branding.theme) || {});
const { locale } = store.getState();
i18n.init({
resources: {
[locale.language]: {
translation: locale.translations,
},
},
lng: locale.language,
interpolation: { escapeValue: false },
});
const sheet = new ServerStyleSheet();
const loginComponent = renderToString(
<StyleSheetManager sheet={sheet.instance}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<ThemeProvider theme={theme}>
<LoginComponent action={action} message={errorMessage} />
</ThemeProvider>
</I18nextProvider>
</Provider>
</StyleSheetManager>,
);
sheet.collectStyles(<LoginComponent action={action} message={errorMessage} />);
const style = sheet.getStyleTags();
sheet.seal();
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>${branding.companyName}</title>
${style}
${faviconTag}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,700" type="text/css">
${styles.join("\n")}
<script src="${h.assetPath("global.bundle.js", assets)}"></script>
<script src="${h.assetPath("design-system.bundle.js", assets)}"></script>
</head>
<body>
<div id="app">${loginComponent}</div>
${scripts.join("\n")}
</body>
</html>
`;
};
export default html;
import React from "react";
import styled, { createGlobalStyle } from "styled-components";
import { useSelector } from "react-redux";
import { Box, H5, Button, Text, MessageBox } from "@adminjs/design-system";
import { ReduxState, useTranslation } from "adminjs";
const GlobalStyle = createGlobalStyle`
html, body, #app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
`;
const Wrapper = styled(Box)`
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
text-align: center;
`;
const StyledLogo = styled.img`
max-width: 200px;
margin: 1em 0;
`;
export type LoginProps = {
message: string | undefined;
action: string;
};
export const Login: React.FC<LoginProps> = (props) => {
const { action, message } = props;
const { translateMessage } = useTranslation();
const branding = useSelector((state: ReduxState) => state.branding);
return (
<React.Fragment>
<GlobalStyle />
<Wrapper flex variant="grey">
<Box bg="white" height="440px" flex boxShadow="login" width={[1, 2 / 3, "auto"]}>
<Box
as="form"
action={action}
method="GET"
p="x3"
flexGrow={1}
width={["100%", "100%", "480px"]}
style={{
alignSelf: "center",
}}>
<H5 marginBottom="xxl">
{branding.logo ? (
<StyledLogo
src={branding.logo}
alt={branding.companyName}
/>
) : (
branding.companyName
)}
</H5>
{message && (
<MessageBox
my="lg"
message={
message.split(" ").length > 1
? message
: translateMessage(message)
}
variant="danger"
/>
)}
<Text mt="xl" textAlign="center">
<Button variant="primary">Login with LoID</Button>
</Text>
</Box>
</Box>
</Wrapper>
</React.Fragment>
);
};
export default Login; Now my users can perfectly login using any authentication method I want :) hope this help y'all |
This looks like exactly what I need, have you put files on GitHub? Also want to make sure I make the right attribution and checking on usage rights. |
I can't say for @AienTech but feel free to use my code - it's just adminjs's source code with very few modifications. |
I tried to use the adminbro nestjs module with oauth authentication. The
auth
option does not work for me because the user should be redirected to an identity server to log in, instead showing the login page. Is there a way I can provide my own authentication router? I noticed that it seems possible with the original adminbro library (SoftwareBrothers/adminjs#546) but I'm not sure how to use it with nestjs module.The text was updated successfully, but these errors were encountered: