diff --git a/package.json b/package.json index 1ec03ce..f93ac8b 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, "homepage": "https://github.com/SoftwareBrothers/adminjs-expressjs#readme", "peerDependencies": { - "adminjs": "^7.0.0", + "adminjs": "^7.4.0", "express": ">=4.18.2", "express-formidable": "^1.2.0", "express-session": ">=1.17.3", @@ -60,7 +60,7 @@ "@types/node": "^18.15.3", "@typescript-eslint/eslint-plugin": "^5.53.0", "@typescript-eslint/parser": "^5.53.0", - "adminjs": "^7.0.0", + "adminjs": "^7.4.0", "commitlint": "^17.4.4", "eslint": "^8.35.0", "eslint-config-airbnb-base": "^15.0.0", diff --git a/src/authentication/login.handler.ts b/src/authentication/login.handler.ts index e726df8..e333d46 100644 --- a/src/authentication/login.handler.ts +++ b/src/authentication/login.handler.ts @@ -70,12 +70,20 @@ export const withLogin = ( const { rootPath } = admin.options; const loginPath = getLoginPath(admin); + const { provider } = auth; + const providerProps = provider?.getUiProps?.() ?? {}; + router.get(loginPath, async (req, res) => { - const login = await admin.renderLogin({ + const baseProps = { action: admin.options.loginPath, errorMessage: null, + }; + const login = await admin.renderLogin({ + ...baseProps, + ...providerProps, }); - res.send(login); + + return res.send(login); }); router.post(loginPath, async (req, res, next) => { @@ -83,34 +91,54 @@ export const withLogin = ( const login = await admin.renderLogin({ action: admin.options.loginPath, errorMessage: "tooManyRequests", + ...providerProps, }); - res.send(login); - return; + + return res.send(login); } - const { email, password } = req.fields as { - email: string; - password: string; - }; + const context: AuthenticationContext = { req, res }; - const adminUser = await auth.authenticate(email, password, context); + + let adminUser; + if (provider) { + adminUser = await provider.handleLogin( + { + headers: req.headers, + query: req.query, + params: req.params, + data: req.fields ?? {}, + }, + context + ); + } else { + const { email, password } = req.fields as { + email: string; + password: string; + }; + // "auth.authenticate" must always be defined if "auth.provider" isn't + adminUser = await auth.authenticate!(email, password, context); + } + if (adminUser) { req.session.adminUser = adminUser; req.session.save((err) => { if (err) { - next(err); + return next(err); } if (req.session.redirectTo) { - res.redirect(302, req.session.redirectTo); + return res.redirect(302, req.session.redirectTo); } else { - res.redirect(302, rootPath); + return res.redirect(302, rootPath); } }); } else { const login = await admin.renderLogin({ action: admin.options.loginPath, errorMessage: "invalidCredentials", + ...providerProps, }); - res.send(login); + + return res.send(login); } }); }; diff --git a/src/authentication/logout.handler.ts b/src/authentication/logout.handler.ts index 4ce2ac0..43ca125 100644 --- a/src/authentication/logout.handler.ts +++ b/src/authentication/logout.handler.ts @@ -1,5 +1,6 @@ import AdminJS from "adminjs"; import { Router } from "express"; +import { AuthenticationOptions } from "../types.js"; const getLogoutPath = (admin: AdminJS) => { const { logoutPath, rootPath } = admin.options; @@ -10,10 +11,20 @@ const getLogoutPath = (admin: AdminJS) => { : `/${normalizedLogoutPath}`; }; -export const withLogout = (router: Router, admin: AdminJS): void => { +export const withLogout = ( + router: Router, + admin: AdminJS, + auth: AuthenticationOptions +): void => { const logoutPath = getLogoutPath(admin); + const { provider } = auth; + router.get(logoutPath, async (request, response) => { + if (provider) { + await provider.handleLogout({ req: request, res: response }); + } + request.session.destroy(() => { response.redirect(admin.options.loginPath); }); diff --git a/src/authentication/refresh.handler.ts b/src/authentication/refresh.handler.ts new file mode 100644 index 0000000..5f7d351 --- /dev/null +++ b/src/authentication/refresh.handler.ts @@ -0,0 +1,61 @@ +import AdminJS, { CurrentAdmin } from "adminjs"; +import { Router } from "express"; +import { AuthenticationOptions } from "../types.js"; +import { WrongArgumentError } from "../errors.js"; + +const getRefreshTokenPath = (admin: AdminJS) => { + const { refreshTokenPath, rootPath } = admin.options; + const normalizedRefreshTokenPath = refreshTokenPath.replace(rootPath, ""); + + return normalizedRefreshTokenPath.startsWith("/") + ? normalizedRefreshTokenPath + : `/${normalizedRefreshTokenPath}`; +}; + +const MISSING_PROVIDER_ERROR = + '"provider" has to be configured to use refresh token mechanism'; + +export const withRefresh = ( + router: Router, + admin: AdminJS, + auth: AuthenticationOptions +): void => { + const refreshTokenPath = getRefreshTokenPath(admin); + + const { provider } = auth; + + router.post(refreshTokenPath, async (request, response) => { + if (!provider) { + throw new WrongArgumentError(MISSING_PROVIDER_ERROR); + } + + const updatedAuthInfo = await provider.handleRefreshToken( + { + data: request.fields ?? {}, + query: request.query, + params: request.params, + headers: request.headers, + }, + { req: request, res: response } + ); + + let admin = request.session.adminUser as Partial | null; + if (!admin) { + admin = {}; + } + + if (!admin._auth) { + admin._auth = {}; + } + + admin._auth = { + ...admin._auth, + ...updatedAuthInfo, + }; + + request.session.adminUser = admin; + request.session.save(() => { + response.send(admin); + }); + }); +}; diff --git a/src/buildAuthenticatedRouter.ts b/src/buildAuthenticatedRouter.ts index 7fae19b..19324f5 100644 --- a/src/buildAuthenticatedRouter.ts +++ b/src/buildAuthenticatedRouter.ts @@ -7,8 +7,14 @@ import { withLogin } from "./authentication/login.handler.js"; import { withLogout } from "./authentication/logout.handler.js"; import { withProtectedRoutesHandler } from "./authentication/protected-routes.handler.js"; import { buildAssets, buildRoutes, initializeAdmin } from "./buildRouter.js"; -import { OldBodyParserUsedError } from "./errors.js"; +import { OldBodyParserUsedError, WrongArgumentError } from "./errors.js"; import { AuthenticationOptions, FormidableOptions } from "./types.js"; +import { withRefresh } from "./authentication/refresh.handler.js"; + +const MISSING_AUTH_CONFIG_ERROR = + 'You must configure either "authenticate" method or assign an auth "provider"'; +const INVALID_AUTH_CONFIG_ERROR = + 'You cannot configure both "authenticate" and "provider". "authenticate" will be removed in next major release.'; /** * @typedef {Function} Authenticate @@ -58,6 +64,21 @@ export const buildAuthenticatedRouter = ( const { routes, assets } = AdminRouter; const router = predefinedRouter || express.Router(); + if (!auth.authenticate && !auth.provider) { + throw new WrongArgumentError(MISSING_AUTH_CONFIG_ERROR); + } + + if (auth.authenticate && auth.provider) { + throw new WrongArgumentError(INVALID_AUTH_CONFIG_ERROR); + } + + if (auth.provider) { + admin.options.env = { + ...admin.options.env, + ...auth.provider.getUiProps(), + }; + } + router.use((req, _, next) => { if ((req as any)._body) { next(new OldBodyParserUsedError()); @@ -76,10 +97,11 @@ export const buildAuthenticatedRouter = ( router.use(formidableMiddleware(formidableOptions) as any); withLogin(router, admin, auth); - withLogout(router, admin); + withLogout(router, admin, auth); buildAssets({ admin, assets, routes, router }); withProtectedRoutesHandler(router, admin); + withRefresh(router, admin, auth); buildRoutes({ admin, routes, router }); return router; diff --git a/src/types.ts b/src/types.ts index a5d2004..1fb83ab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import { BaseAuthProvider } from "adminjs"; import { Request, Response } from "express"; export type FormidableOptions = { @@ -37,7 +38,7 @@ export type AuthenticationMaxRetriesOptions = { export type AuthenticationOptions = { cookiePassword: string; cookieName?: string; - authenticate: ( + authenticate?: ( email: string, password: string, context?: AuthenticationContext @@ -46,4 +47,5 @@ export type AuthenticationOptions = { * @description Maximum number of authorization attempts (if number - per minute) */ maxRetries?: number | AuthenticationMaxRetriesOptions; + provider?: BaseAuthProvider; }; diff --git a/yarn.lock b/yarn.lock index 016ab62..eb0b50a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3583,44 +3583,6 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== -"@types/babel-core@^6.25.7": - version "6.25.7" - resolved "https://registry.yarnpkg.com/@types/babel-core/-/babel-core-6.25.7.tgz#f9c22d5c085686da2f6ffbdae778edb3e6017671" - integrity sha512-WPnyzNFVRo6bxpr7bcL27qXtNKNQ3iToziNBpibaXHyKGWQA0+tTLt73QQxC/5zzbM544ih6Ni5L5xrck6rGwg== - dependencies: - "@types/babel-generator" "*" - "@types/babel-template" "*" - "@types/babel-traverse" "*" - "@types/babel-types" "*" - "@types/babylon" "*" - -"@types/babel-generator@*": - version "6.25.5" - resolved "https://registry.yarnpkg.com/@types/babel-generator/-/babel-generator-6.25.5.tgz#b02723fd589349b05524376e5530228d3675d878" - integrity sha512-lhbwMlAy5rfWG+R6l8aPtJdEFX/kcv6LMFIuvUb0i89ehqgD24je9YcB+0fRspQhgJGlEsUImxpw4pQeKS/+8Q== - dependencies: - "@types/babel-types" "*" - -"@types/babel-template@*": - version "6.25.2" - resolved "https://registry.yarnpkg.com/@types/babel-template/-/babel-template-6.25.2.tgz#3c4cde02dbcbbf461a58d095a9f69f35eabd5f06" - integrity sha512-QKtDQRJmAz3Y1HSxfMl0syIHebMc/NnOeH/8qeD0zjgU2juD0uyC922biMxCy5xjTNvHinigML2l8kxE8eEBmw== - dependencies: - "@types/babel-types" "*" - "@types/babylon" "*" - -"@types/babel-traverse@*": - version "6.25.7" - resolved "https://registry.yarnpkg.com/@types/babel-traverse/-/babel-traverse-6.25.7.tgz#bc75fce23d8394534562a36a32dec94a54d11835" - integrity sha512-BeQiEGLnVzypzBdsexEpZAHUx+WucOMXW6srEWDkl4SegBlaCy+iBvRO+4vz6EZ+BNQg22G4MCdDdvZxf+jW5A== - dependencies: - "@types/babel-types" "*" - -"@types/babel-types@*": - version "7.0.11" - resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.11.tgz#263b113fa396fac4373188d73225297fb86f19a9" - integrity sha512-pkPtJUUY+Vwv6B1inAz55rQvivClHJxc9aVEPPmaq2cbyeMLCiDpbKpcKyX4LAwpNGi+SHBv0tHv6+0gXv0P2A== - "@types/babel__core@^7.1.14": version "7.20.0" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" @@ -3654,13 +3616,6 @@ dependencies: "@babel/types" "^7.3.0" -"@types/babylon@*": - version "6.16.6" - resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.6.tgz#a1e7e01567b26a5ebad321a74d10299189d8d932" - integrity sha512-G4yqdVlhr6YhzLXFKy5F7HtRBU8Y23+iWy7UKthMq/OSQnL1hbsoeXESQ2LY8zEDlknipDG3nRGhUC9tkwvy/w== - dependencies: - "@types/babel-types" "*" - "@types/body-parser@*": version "1.19.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" @@ -3880,15 +3835,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^18.0.28": - version "18.0.28" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" - integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/resolve@1.20.2": version "1.20.2" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" @@ -4084,10 +4030,10 @@ acorn@^8.4.1, acorn@^8.5.0, acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== -adminjs@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/adminjs/-/adminjs-7.0.0.tgz#5dad16fcdd91dfe9fd84402b3e109f9fdbb74534" - integrity sha512-6cvr04yhPpoqpK9lfy5ohxHMUI+J9lDZbRScyqzmpPTZ4P8E68unZekixx7nAGXFBmhixP5+CumLNpCNzcUeGA== +adminjs@^7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/adminjs/-/adminjs-7.4.0.tgz#9551c79ac1b6047f1cc86ac1525e01660fea954a" + integrity sha512-GKot4WNEe5aQN2MLkSR216N0oE9KrpJ+COwPrYhRlF42wUMiQucwQbq36VfMb/ZsiEpF3SfBdSa9Qi6EApR0WQ== dependencies: "@adminjs/design-system" "^4.0.0" "@babel/core" "^7.21.0" @@ -4106,8 +4052,6 @@ adminjs@^7.0.0: "@rollup/plugin-node-resolve" "^15.0.1" "@rollup/plugin-replace" "^5.0.2" "@rollup/plugin-terser" "^0.4.0" - "@types/babel-core" "^6.25.7" - "@types/react" "^18.0.28" axios "^1.3.4" commander "^10.0.0" flat "^5.0.2" @@ -4118,6 +4062,7 @@ adminjs@^7.0.0: ora "^6.2.0" prop-types "^15.8.1" punycode "^2.3.0" + qs "^6.11.1" react "^18.2.0" react-dom "^18.2.0" react-feather "^2.0.10" @@ -10684,6 +10629,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.11.1: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"