├── .npmrc ├── .vscode └── settings.json ├── commitlint.config.cjs ├── tsconfig.test.json ├── src ├── convertRoutes.ts ├── typings │ └── express-session │ │ └── index.d.ts ├── logger.ts ├── authentication │ ├── protected-routes.handler.ts │ ├── logout.handler.ts │ ├── refresh.handler.ts │ └── login.handler.ts ├── errors.ts ├── types.ts ├── buildAuthenticatedRouter.ts ├── buildRouter.ts └── index.ts ├── nodemon.json ├── nodemon.auth.json ├── examples ├── mongoose │ ├── admin-model.ts │ └── article-model.ts ├── simple.ts └── auth.ts ├── test └── plugin.test.ts ├── .eslintrc.cjs ├── .releaserc ├── jest.json ├── tsconfig.json ├── LICENSE ├── README.md ├── .gitignore ├── .github └── workflows │ └── push.yml └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | access=public -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '@commitlint/config-conventional', 4 | ], 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src/**/*.ts", "./test/**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/convertRoutes.ts: -------------------------------------------------------------------------------- 1 | export const convertToExpressRoute = (adminRoute: string): string => 2 | adminRoute.replace(/{/g, ":").replace(/}/g, ""); 3 | -------------------------------------------------------------------------------- /src/typings/express-session/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare module "express-session" { 4 | interface SessionData { 5 | adminUser?: unknown; 6 | redirectTo?: string; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": ["*.test.*", "*.spec.*"], 4 | "exec": "ts-node --files examples/simple.ts", 5 | "watch": [ 6 | "src", 7 | "examples" 8 | ] 9 | } -------------------------------------------------------------------------------- /nodemon.auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": ["*.test.*", "*.spec.*"], 4 | "exec": "ts-node --files examples/auth.ts", 5 | "watch": [ 6 | "src", 7 | "examples" 8 | ], 9 | "ext": "ts" 10 | } -------------------------------------------------------------------------------- /examples/mongoose/admin-model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema } = mongoose; 4 | 5 | const AdminSchema = new Schema({ 6 | email: String, 7 | password: String, 8 | }); 9 | 10 | const Admin = mongoose.model("Admin", AdminSchema); 11 | 12 | module.exports = Admin; 13 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | export const log = { 2 | /** 3 | * Logs the debug message to console if `process.env.ADMINJS_EXPRESS_DEBUG` is set 4 | */ 5 | debug: (message: string): void => { 6 | if (process.env.ADMINJS_EXPRESS_DEBUG) { 7 | console.debug(message); 8 | } 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /examples/mongoose/article-model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const { Schema } = mongoose; 4 | 5 | const ArticleSchema = new Schema({ 6 | title: String, 7 | content: String, 8 | author: String, 9 | createdAt: Date, 10 | published: Boolean, 11 | }); 12 | 13 | const Article = mongoose.model("Article", ArticleSchema); 14 | 15 | module.exports = Article; 16 | -------------------------------------------------------------------------------- /test/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import { AdminJS } from "adminjs"; 2 | import express from "express"; 3 | import { jest } from "@jest/globals"; 4 | 5 | import { buildRouter } from "../src/buildRouter"; 6 | 7 | jest.useFakeTimers(); 8 | 9 | describe("plugin", () => { 10 | describe(".buildRouter", () => { 11 | it("returns an express router when AdminJS instance given as an argument", () => { 12 | expect(buildRouter(new AdminJS())).toBeInstanceOf( 13 | express.Router().constructor 14 | ); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | plugins: ["@typescript-eslint/eslint-plugin"], 4 | extends: [ 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:prettier/recommended", 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: "module", 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | }, 15 | rules: { 16 | "prettier/prettier": "error", 17 | }, 18 | ignorePatterns: ["node_modules", "lib", "types"], 19 | settings: { 20 | react: { 21 | version: "detect", 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "+([0-9])?(.{+([0-9]),x}).x", 4 | "master", 5 | "next", 6 | "next-major", 7 | { 8 | "name": "beta", 9 | "prerelease": true 10 | } 11 | ], 12 | "plugins": [ 13 | "@semantic-release/commit-analyzer", 14 | "@semantic-release/release-notes-generator", 15 | "@semantic-release/npm", 16 | "@semantic-release/github", 17 | "@semantic-release/git", 18 | [ 19 | "semantic-release-slack-bot", 20 | { 21 | "notifyOnSuccess": true, 22 | "notifyOnFail": false 23 | } 24 | ] 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/authentication/protected-routes.handler.ts: -------------------------------------------------------------------------------- 1 | import AdminJS from "adminjs"; 2 | import { Router, RequestHandler } from "express"; 3 | 4 | export const withProtectedRoutesHandler = ( 5 | router: Router, 6 | admin: AdminJS 7 | ): void => { 8 | const { loginPath } = admin.options; 9 | const authorizedRoutesMiddleware: RequestHandler = ( 10 | request, 11 | response, 12 | next 13 | ) => { 14 | if (!request.session || !request.session.adminUser) { 15 | return response.redirect(loginPath); 16 | } 17 | return next(); 18 | }; 19 | 20 | router.use(authorizedRoutesMiddleware); 21 | }; 22 | -------------------------------------------------------------------------------- /jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts", "tsx"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".test.ts$", 6 | "extensionsToTreatAsEsm": [".ts"], 7 | "transformIgnorePatterns": ["node_modules"], 8 | "transform": { 9 | "^.+\\.(t|j)sx?$": [ 10 | "ts-jest", 11 | { 12 | "useESM": true, 13 | "tsconfig": "./tsconfig.test.json", 14 | "isolatedModules": true 15 | } 16 | ] 17 | }, 18 | "moduleNameMapper": { 19 | "^(\\.{1,2}/.*)\\.js$": "$1" 20 | }, 21 | "testTimeout": 10000, 22 | "preset": "ts-jest/presets/default-esm", 23 | "verbose": true, 24 | "silent": true, 25 | "forceExit": true 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "nodenext", 4 | "sourceMap": false, 5 | "outDir": "lib", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "declaration": true, 9 | "types": ["express-formidable", "jest", "./src/typings/express-session"], 10 | "resolveJsonModule": true, 11 | "target": "es2017", 12 | "jsx": "preserve", 13 | "importHelpers": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": true, 16 | "strictPropertyInitialization": true, 17 | "strictFunctionTypes": true, 18 | "strictBindCallApply": true, 19 | "noImplicitThis": true, 20 | "moduleResolution": "nodenext", 21 | "baseUrl": "./", 22 | "typeRoots": ["./src/typings", "./node_modules/@types"], 23 | "allowJs": true 24 | }, 25 | "exclude": ["lib", "types", "test", "examples", "commitlint.config.cjs"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/simple.ts: -------------------------------------------------------------------------------- 1 | import AdminJS from "adminjs"; 2 | import express from "express"; 3 | import mongoose from "mongoose"; 4 | import MongooseAdapter from "@adminjs/mongoose"; 5 | 6 | import AdminJSExpress from "../src/index.js"; 7 | import "./mongoose/article-model.js"; 8 | import "./mongoose/admin-model.js"; 9 | 10 | AdminJS.registerAdapter(MongooseAdapter); 11 | 12 | const start = async () => { 13 | const connection = await mongoose.connect( 14 | process.env.MONGO_URL || "mongodb://localhost:27017/example" 15 | ); 16 | const app = express(); 17 | 18 | const adminJs = new AdminJS({ 19 | databases: [connection], 20 | rootPath: "/admin", 21 | }); 22 | const router = AdminJSExpress.buildRouter(adminJs); 23 | 24 | app.use(adminJs.options.rootPath, router); 25 | 26 | app.listen(process.env.PORT || 8080, () => 27 | console.log("AdminJS is running under localhost:8080/admin") 28 | ); 29 | }; 30 | 31 | start(); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 SoftwareBrothers.co 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/authentication/logout.handler.ts: -------------------------------------------------------------------------------- 1 | import AdminJS from "adminjs"; 2 | import { Router } from "express"; 3 | import { AuthenticationOptions } from "../types.js"; 4 | 5 | const getLogoutPath = (admin: AdminJS) => { 6 | const { logoutPath, rootPath } = admin.options; 7 | const normalizedLogoutPath = logoutPath.replace(rootPath, ""); 8 | 9 | return normalizedLogoutPath.startsWith("/") 10 | ? normalizedLogoutPath 11 | : `/${normalizedLogoutPath}`; 12 | }; 13 | 14 | export const withLogout = ( 15 | router: Router, 16 | admin: AdminJS, 17 | auth: AuthenticationOptions 18 | ): void => { 19 | const logoutPath = getLogoutPath(admin); 20 | 21 | const { provider } = auth; 22 | 23 | router.get(logoutPath, async (request, response) => { 24 | if (provider) { 25 | try { 26 | await provider.handleLogout({ req: request, res: response }); 27 | } catch (error) { 28 | console.error(error); // fail silently and still logout 29 | } 30 | } 31 | 32 | request.session.destroy(() => { 33 | response.redirect(admin.options.loginPath); 34 | }); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export const MISSING_AUTH_CONFIG_ERROR = 2 | 'You must configure either "authenticate" method or assign an auth "provider"'; 3 | export const INVALID_AUTH_CONFIG_ERROR = 4 | 'You cannot configure both "authenticate" and "provider". "authenticate" will be removed in next major release.'; 5 | 6 | export class WrongArgumentError extends Error { 7 | constructor(message: string) { 8 | super(message); 9 | this.name = "WrongArgumentError"; 10 | } 11 | } 12 | 13 | export class OldBodyParserUsedError extends Error { 14 | constructor( 15 | message = ` 16 | You probably used old \`body-parser\` middleware, which is not compatible 17 | with @adminjs/express. In order to make it work you will have to 18 | 1. move body-parser invocation after the AdminJS setup like this: 19 | 20 | const adminJs = new AdminJS() 21 | const router = new buildRouter(adminJs) 22 | app.use(adminJs.options.rootPath, router) 23 | 24 | // body parser goes after the AdminJS router 25 | app.use(bodyParser()) 26 | 27 | 2. Upgrade body-parser to the latest version and use it like this: 28 | app.use(bodyParser.json()) 29 | ` 30 | ) { 31 | super(message); 32 | this.name = "WrongArgumentError"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { BaseAuthProvider } from "adminjs"; 2 | import { Request, Response } from "express"; 3 | 4 | export type FormidableOptions = { 5 | encoding?: string; 6 | uploadDir?: string; 7 | keepExtensions?: boolean; 8 | type?: "multipart" | "urlencoded"; 9 | maxFileSize?: number; 10 | maxFieldsSize?: number; 11 | maxFields?: number; 12 | hash?: boolean | "sha1" | "md5"; 13 | multiples?: boolean; 14 | }; 15 | 16 | export type AuthenticationContext = { 17 | /** 18 | * @description Authentication request object 19 | */ 20 | req: Request; 21 | /** 22 | * @description Authentication response object 23 | */ 24 | res: Response; 25 | }; 26 | 27 | export type AuthenticationMaxRetriesOptions = { 28 | /** 29 | * @description Count of retries 30 | */ 31 | count: number; 32 | /** 33 | * @description Time to reset (in seconds) 34 | */ 35 | duration: number; 36 | }; 37 | 38 | export type AuthenticationOptions = { 39 | cookiePassword: string; 40 | cookieName?: string; 41 | authenticate?: ( 42 | email: string, 43 | password: string, 44 | context?: AuthenticationContext 45 | ) => unknown | null; 46 | /** 47 | * @description Maximum number of authorization attempts (if number - per minute) 48 | */ 49 | maxRetries?: number | AuthenticationMaxRetriesOptions; 50 | provider?: BaseAuthProvider; 51 | }; 52 | -------------------------------------------------------------------------------- /examples/auth.ts: -------------------------------------------------------------------------------- 1 | import MongooseAdapter from "@adminjs/mongoose"; 2 | import AdminJS from "adminjs"; 3 | import express from "express"; 4 | import mongoose from "mongoose"; 5 | 6 | import AdminJSExpress from "../src/index.js"; 7 | import "./mongoose/admin-model.js"; 8 | import "./mongoose/article-model.js"; 9 | 10 | AdminJS.registerAdapter(MongooseAdapter); 11 | 12 | const ADMIN = { 13 | email: "test@example.com", 14 | password: "password", 15 | }; 16 | 17 | const start = async () => { 18 | const connection = await mongoose.connect( 19 | process.env.MONGO_URL || "mongodb://localhost:27017/example" 20 | ); 21 | const app = express(); 22 | 23 | const adminJs = new AdminJS({ 24 | databases: [connection], 25 | rootPath: "/admin", 26 | }); 27 | 28 | const router = AdminJSExpress.buildAuthenticatedRouter(adminJs, { 29 | authenticate: async (email, password) => { 30 | if (ADMIN.password === password && ADMIN.email === email) { 31 | return ADMIN; 32 | } 33 | return null; 34 | }, 35 | cookiePassword: "somasd1nda0asssjsdhb21uy3g", 36 | maxRetries: { 37 | count: 3, 38 | duration: 120, 39 | }, 40 | }); 41 | 42 | app.use(adminJs.options.rootPath, router); 43 | 44 | app.listen(process.env.PORT || 8080, () => 45 | console.log("AdminJS is running under localhost:8080/admin") 46 | ); 47 | }; 48 | 49 | start(); 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expressjs plugin for AdminJS 2 | 3 | This is an official [AdminJS](https://github.com/SoftwareBrothers/adminjs) plugin which integrates it to [expressjs](https://expressjs.com/) framework. 4 | 5 | ## AdminJS 6 | 7 | AdminJS is an automatic admin interface which can be plugged into your application. You, as a developer, provide database models (like posts, comments, stores, products or whatever else your application uses), and AdminJS generates UI which allows you (or other trusted users) to manage content. 8 | 9 | Check out the example application with mongo and postgres models here: https://adminjs-demo.herokuapp.com/admin/ 10 | 11 | Or visit [AdminJS](https://github.com/SoftwareBrothers/adminjs) github page. 12 | 13 | ## Usage 14 | 15 | To see example usage visit the [Express section under AdminJS project page](https://docs.adminjs.co/installation/plugins/express) 16 | 17 | ## Debugging 18 | Set `process.env.ADMINJS_EXPRESS_DEBUG` env variable to see debug logs from the library 19 | 20 | ## License 21 | 22 | AdminJS is copyrighted © 2023 rst.software. It is a free software, and may be redistributed under the terms specified in the [LICENSE](LICENSE.md) file. 23 | 24 | ## About rst.software 25 | 26 | 27 | 28 | We’re an open, friendly team that helps clients from all over the world to transform their businesses and create astonishing products. 29 | 30 | * We are available for [hire](https://www.rst.software/estimate-your-project). 31 | * If you want to work for us - check out the [career page](https://www.rst.software/join-us). 32 | -------------------------------------------------------------------------------- /src/authentication/refresh.handler.ts: -------------------------------------------------------------------------------- 1 | import AdminJS, { CurrentAdmin } from "adminjs"; 2 | import { Router } from "express"; 3 | import { AuthenticationOptions } from "../types.js"; 4 | import { WrongArgumentError } from "../errors.js"; 5 | 6 | const getRefreshTokenPath = (admin: AdminJS) => { 7 | const { refreshTokenPath, rootPath } = admin.options; 8 | const normalizedRefreshTokenPath = refreshTokenPath.replace(rootPath, ""); 9 | 10 | return normalizedRefreshTokenPath.startsWith("/") 11 | ? normalizedRefreshTokenPath 12 | : `/${normalizedRefreshTokenPath}`; 13 | }; 14 | 15 | const MISSING_PROVIDER_ERROR = 16 | '"provider" has to be configured to use refresh token mechanism'; 17 | 18 | export const withRefresh = ( 19 | router: Router, 20 | admin: AdminJS, 21 | auth: AuthenticationOptions 22 | ): void => { 23 | const refreshTokenPath = getRefreshTokenPath(admin); 24 | 25 | const { provider } = auth; 26 | 27 | router.post(refreshTokenPath, async (request, response) => { 28 | if (!provider) { 29 | throw new WrongArgumentError(MISSING_PROVIDER_ERROR); 30 | } 31 | 32 | const updatedAuthInfo = await provider.handleRefreshToken( 33 | { 34 | data: request.fields ?? {}, 35 | query: request.query, 36 | params: request.params, 37 | headers: request.headers, 38 | }, 39 | { req: request, res: response } 40 | ); 41 | 42 | let admin = request.session.adminUser as Partial | null; 43 | if (!admin) { 44 | admin = {}; 45 | } 46 | 47 | if (!admin._auth) { 48 | admin._auth = {}; 49 | } 50 | 51 | admin._auth = { 52 | ...admin._auth, 53 | ...updatedAuthInfo, 54 | }; 55 | 56 | request.session.adminUser = admin; 57 | request.session.save(() => { 58 | response.send(admin); 59 | }); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | .DS_store 119 | 120 | # build files 121 | /lib 122 | .idea -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | on: push 3 | jobs: 4 | setup: 5 | name: setup 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Dump GitHub context 9 | env: 10 | GITHUB_CONTEXT: ${{ toJson(github) }} 11 | run: echo "$GITHUB_CONTEXT" 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Setup 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: "18.x" 18 | - uses: actions/cache@v2 19 | id: yarn-cache 20 | with: 21 | path: node_modules 22 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} 23 | restore-keys: | 24 | ${{ runner.os }}-node_modules- 25 | - name: Install 26 | if: steps.yarn-cache.outputs.cache-hit != 'true' 27 | run: yarn install 28 | 29 | test: 30 | name: Test 31 | runs-on: ubuntu-latest 32 | needs: setup 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v2 36 | - name: Setup 37 | uses: actions/setup-node@v2 38 | with: 39 | node-version: "18.x" 40 | - uses: actions/cache@v2 41 | id: yarn-cache 42 | with: 43 | path: node_modules 44 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} 45 | restore-keys: | 46 | ${{ runner.os }}-node_modules- 47 | - name: Install 48 | if: steps.yarn-cache.outputs.cache-hit != 'true' 49 | run: yarn install 50 | - name: Lint 51 | run: yarn lint 52 | - name: Build 53 | run: yarn build 54 | - name: test 55 | run: yarn test 56 | 57 | publish: 58 | name: Publish 59 | needs: test 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v2 64 | - name: Setup 65 | uses: actions/setup-node@v2 66 | with: 67 | node-version: "18.x" 68 | - uses: actions/cache@v2 69 | id: yarn-cache 70 | with: 71 | path: node_modules 72 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} 73 | restore-keys: | 74 | ${{ runner.os }}-node_modules- 75 | - name: Install 76 | if: steps.yarn-cache.outputs.cache-hit != 'true' 77 | run: yarn install 78 | - name: Build 79 | run: yarn build 80 | - name: Release 81 | env: 82 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} 85 | JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} 86 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 87 | run: yarn release 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adminjs/express", 3 | "version": "6.1.1", 4 | "description": "This is an official AdminJS plugin which integrates it with Express.js framework", 5 | "main": "lib/index.js", 6 | "type": "module", 7 | "exports": { 8 | ".": { 9 | "import": "./lib/index.js", 10 | "types": "./lib/index.d.ts" 11 | } 12 | }, 13 | "scripts": { 14 | "dev": "rm -rf lib && tsc --watch", 15 | "build": "rm -rf lib && tsc", 16 | "example:simple": "nodemon", 17 | "example:auth": "nodemon --config nodemon.auth.json", 18 | "test": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.json --runInBand --detectOpenHandles", 19 | "lint": "eslint './**/*.ts'", 20 | "check:all": "yarn lint && yarn build && yarn test", 21 | "release": "semantic-release" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/SoftwareBrothers/adminjs-expressjs.git" 26 | }, 27 | "keywords": [ 28 | "expressjs", 29 | "admin", 30 | "adminjs", 31 | "admin-panel" 32 | ], 33 | "author": "Michał Laskowski", 34 | "license": "SEE LICENSE IN LICENSE", 35 | "bugs": { 36 | "url": "https://github.com/SoftwareBrothers/adminjs-expressjs/issues" 37 | }, 38 | "homepage": "https://github.com/SoftwareBrothers/adminjs-expressjs#readme", 39 | "peerDependencies": { 40 | "adminjs": "^7.4.0", 41 | "express": ">=4.18.2", 42 | "express-formidable": "^1.2.0", 43 | "express-session": ">=1.17.3", 44 | "tslib": "^2.5.0" 45 | }, 46 | "husky": { 47 | "hooks": { 48 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 49 | } 50 | }, 51 | "devDependencies": { 52 | "@adminjs/mongoose": "^4.0.0", 53 | "@commitlint/config-conventional": "^17.4.4", 54 | "@jest/globals": "^29.5.0", 55 | "@semantic-release/git": "^9.0.0", 56 | "@types/express": "^4.17.17", 57 | "@types/express-formidable": "^1.2.0", 58 | "@types/express-session": "^1.17.6", 59 | "@types/jest": "^26.0.15", 60 | "@types/node": "^18.15.3", 61 | "@typescript-eslint/eslint-plugin": "^5.53.0", 62 | "@typescript-eslint/parser": "^5.53.0", 63 | "adminjs": "^7.4.0", 64 | "commitlint": "^17.4.4", 65 | "eslint": "^8.35.0", 66 | "eslint-config-airbnb-base": "^15.0.0", 67 | "eslint-config-prettier": "^8.6.0", 68 | "eslint-plugin-import": "^2.27.5", 69 | "eslint-plugin-prettier": "^4.2.1", 70 | "express": "^4.18.2", 71 | "express-formidable": "^1.2.0", 72 | "express-session": "^1.17.3", 73 | "husky": "^4.3.0", 74 | "jest": "^29.5.0", 75 | "mongoose": "^6.10.0", 76 | "nodemon": "^2.0.6", 77 | "prettier": "^2.8.4", 78 | "semantic-release": "^17.2.4", 79 | "semantic-release-slack-bot": "^1.6.2", 80 | "ts-jest": "^29.0.5", 81 | "ts-node": "^10.9.1", 82 | "typescript": "^4.9.5" 83 | }, 84 | "dependencies": {} 85 | } 86 | -------------------------------------------------------------------------------- /src/buildAuthenticatedRouter.ts: -------------------------------------------------------------------------------- 1 | import AdminJS, { Router as AdminRouter } from "adminjs"; 2 | import express, { Router } from "express"; 3 | import formidableMiddleware from "express-formidable"; 4 | import session from "express-session"; 5 | 6 | import { withLogin } from "./authentication/login.handler.js"; 7 | import { withLogout } from "./authentication/logout.handler.js"; 8 | import { withProtectedRoutesHandler } from "./authentication/protected-routes.handler.js"; 9 | import { buildAssets, buildRoutes, initializeAdmin } from "./buildRouter.js"; 10 | import { 11 | INVALID_AUTH_CONFIG_ERROR, 12 | MISSING_AUTH_CONFIG_ERROR, 13 | OldBodyParserUsedError, 14 | WrongArgumentError, 15 | } from "./errors.js"; 16 | import { AuthenticationOptions, FormidableOptions } from "./types.js"; 17 | import { withRefresh } from "./authentication/refresh.handler.js"; 18 | 19 | /** 20 | * @typedef {Function} Authenticate 21 | * @memberof module:@adminjs/express 22 | * @description 23 | * function taking 2 arguments email and password 24 | * @param {string} [email] email given in the form 25 | * @param {string} [password] password given in the form 26 | * @return {CurrentAdmin | null} returns current admin or null 27 | */ 28 | 29 | /** 30 | * Builds the Express Router which is protected by a session auth 31 | * 32 | * Using the router requires you to install `express-session` as a 33 | * dependency. Normally express-session holds session in memory, which is 34 | * not optimized for production usage and, in development, it causes 35 | * logging out after every page refresh (if you use nodemon). 36 | * @static 37 | * @memberof module:@adminjs/express 38 | * @example 39 | * const ADMIN = { 40 | * email: 'test@example.com', 41 | * password: 'password', 42 | * } 43 | * 44 | * AdminJSExpress.buildAuthenticatedRouter(adminJs, { 45 | * authenticate: async (email, password) => { 46 | * if (ADMIN.password === password && ADMIN.email === email) { 47 | * return ADMIN 48 | * } 49 | * return null 50 | * }, 51 | * cookieName: 'adminjs', 52 | * cookiePassword: 'somePassword', 53 | * }, [router]) 54 | */ 55 | export const buildAuthenticatedRouter = ( 56 | admin: AdminJS, 57 | auth: AuthenticationOptions, 58 | predefinedRouter?: express.Router | null, 59 | sessionOptions?: session.SessionOptions, 60 | formidableOptions?: FormidableOptions 61 | ): Router => { 62 | initializeAdmin(admin); 63 | 64 | const { routes, assets } = AdminRouter; 65 | const router = predefinedRouter || express.Router(); 66 | 67 | if (!auth.authenticate && !auth.provider) { 68 | throw new WrongArgumentError(MISSING_AUTH_CONFIG_ERROR); 69 | } 70 | 71 | if (auth.authenticate && auth.provider) { 72 | throw new WrongArgumentError(INVALID_AUTH_CONFIG_ERROR); 73 | } 74 | 75 | if (auth.provider) { 76 | admin.options.env = { 77 | ...admin.options.env, 78 | ...auth.provider.getUiProps(), 79 | }; 80 | } 81 | 82 | router.use((req, _, next) => { 83 | if ((req as any)._body) { 84 | next(new OldBodyParserUsedError()); 85 | } 86 | next(); 87 | }); 88 | 89 | // todo fix types 90 | router.use( 91 | session({ 92 | ...sessionOptions, 93 | secret: auth.cookiePassword, 94 | name: auth.cookieName || "adminjs", 95 | }) as any 96 | ); 97 | router.use(formidableMiddleware(formidableOptions) as any); 98 | 99 | withLogin(router, admin, auth); 100 | withLogout(router, admin, auth); 101 | buildAssets({ admin, assets, routes, router }); 102 | 103 | withProtectedRoutesHandler(router, admin); 104 | withRefresh(router, admin, auth); 105 | buildRoutes({ admin, routes, router }); 106 | 107 | return router; 108 | }; 109 | -------------------------------------------------------------------------------- /src/buildRouter.ts: -------------------------------------------------------------------------------- 1 | import AdminJS, { Router as AdminRouter } from "adminjs"; 2 | import { RequestHandler, Router } from "express"; 3 | import formidableMiddleware from "express-formidable"; 4 | import path from "path"; 5 | 6 | import { WrongArgumentError } from "./errors.js"; 7 | import { log } from "./logger.js"; 8 | import { FormidableOptions } from "./types.js"; 9 | import { convertToExpressRoute } from "./convertRoutes.js"; 10 | 11 | const INVALID_ADMINJS_INSTANCE = 12 | "You have to pass an instance of AdminJS to the buildRouter() function"; 13 | 14 | export type RouteHandlerArgs = { 15 | admin: AdminJS; 16 | route: (typeof AdminRouter)["routes"][0]; 17 | }; 18 | 19 | export type BuildRoutesArgs = { 20 | admin: AdminJS; 21 | routes: (typeof AdminRouter)["routes"]; 22 | router: Router; 23 | }; 24 | 25 | export type BuildAssetsArgs = { 26 | admin: AdminJS; 27 | assets: (typeof AdminRouter)["assets"]; 28 | routes: (typeof AdminRouter)["routes"]; 29 | router: Router; 30 | }; 31 | 32 | export const initializeAdmin = (admin: AdminJS): void => { 33 | if (admin?.constructor?.name !== "AdminJS") { 34 | throw new WrongArgumentError(INVALID_ADMINJS_INSTANCE); 35 | } 36 | 37 | admin.initialize().then(() => { 38 | log.debug("AdminJS: bundle ready"); 39 | }); 40 | }; 41 | 42 | export const routeHandler = 43 | ({ admin, route }: RouteHandlerArgs): RequestHandler => 44 | async (req, res, next) => { 45 | try { 46 | const controller = new route.Controller( 47 | { admin }, 48 | req.session && req.session.adminUser 49 | ); 50 | const { params, query } = req; 51 | const method = req.method.toLowerCase(); 52 | const payload = { 53 | ...(req.fields || {}), 54 | ...(req.files || {}), 55 | }; 56 | const html = await controller[route.action]( 57 | { 58 | ...req, 59 | params, 60 | query, 61 | payload, 62 | method, 63 | }, 64 | res 65 | ); 66 | if (route.contentType) { 67 | res.set({ "Content-Type": route.contentType }); 68 | } 69 | if (html) { 70 | res.send(html); 71 | } 72 | } catch (e) { 73 | next(e); 74 | } 75 | }; 76 | 77 | export const buildRoute = ({ 78 | route, 79 | router, 80 | admin, 81 | }: { 82 | route: (typeof AdminRouter)["routes"][number]; 83 | router: Router; 84 | admin: AdminJS; 85 | }) => { 86 | // we have to change routes defined in AdminJS from {recordId} to :recordId 87 | const expressPath = convertToExpressRoute(route.path); 88 | 89 | if (route.method === "GET") { 90 | router.get(expressPath, routeHandler({ admin, route })); 91 | } 92 | 93 | if (route.method === "POST") { 94 | router.post(expressPath, routeHandler({ admin, route })); 95 | } 96 | }; 97 | 98 | export const buildRoutes = ({ 99 | admin, 100 | routes, 101 | router, 102 | }: BuildRoutesArgs): void => { 103 | routes.forEach((route) => buildRoute({ route, router, admin })); 104 | }; 105 | 106 | export const buildAssets = ({ 107 | admin, 108 | assets, 109 | routes, 110 | router, 111 | }: BuildAssetsArgs): void => { 112 | // Note: We want components.bundle.js to be globally available. In production it is served as a .js asset, meanwhile 113 | // in local environments it's a route with "bundleComponents" action assigned. 114 | const componentBundlerRoute = routes.find( 115 | (r) => r.action === "bundleComponents" 116 | ); 117 | if (componentBundlerRoute) { 118 | buildRoute({ route: componentBundlerRoute, router, admin }); 119 | } 120 | 121 | assets.forEach((asset) => { 122 | router.get(asset.path, async (_req, res) => { 123 | res.sendFile(path.resolve(asset.src)); 124 | }); 125 | }); 126 | }; 127 | 128 | export const buildRouter = ( 129 | admin: AdminJS, 130 | predefinedRouter?: Router | null, 131 | formidableOptions?: FormidableOptions 132 | ): Router => { 133 | initializeAdmin(admin); 134 | 135 | const { routes, assets } = AdminRouter; 136 | const router = predefinedRouter ?? Router(); 137 | // todo fix types 138 | router.use(formidableMiddleware(formidableOptions) as any); 139 | 140 | buildAssets({ admin, assets, routes, router }); 141 | buildRoutes({ admin, routes, router }); 142 | 143 | return router; 144 | }; 145 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import { buildAuthenticatedRouter } from "./buildAuthenticatedRouter.js"; 3 | import { buildRouter } from "./buildRouter.js"; 4 | 5 | /** 6 | * @module @adminjs/express 7 | * @subcategory Plugins 8 | * @section modules 9 | * 10 | * @classdesc 11 | * Plugin that allows you to add AdminJS to Express.js applications. 12 | * 13 | * ## Installation 14 | * 15 | * ```sh 16 | * npm install @adminjs/express 17 | * ``` 18 | * 19 | * It has 2 peerDependencies: `express-formidable` and `express`, 20 | * so you have to install them as well (if they are not installed already) 21 | * 22 | * ``` 23 | * npm install express express-formidable 24 | * ``` 25 | * 26 | * ## Usage 27 | * 28 | * ``` 29 | * const AdminJSExpress = require('@adminjs/express') 30 | * ``` 31 | * 32 | * It exposes 2 methods that create an Express Router, which can be attached 33 | * to a given url in the API. Each method takes a pre-configured instance of {@link AdminJS}. 34 | * 35 | * - {@link module:@adminjs/express.buildRouter AdminJSExpress.buildRouter(admin, [predefinedRouter])} 36 | * - {@link module:@adminjs/express.buildAuthenticatedRouter AdminJSExpress.buildAuthenticatedRouter(admin, auth, [predefinedRouter], sessionOptions)} 37 | * 38 | * If you want to use a router you have already created - not a problem. Just pass it 39 | * as a `predefinedRouter` parameter. 40 | * 41 | * You may want to use this option when you want to include 42 | * some custom auth middleware for you AdminJS routes. 43 | * 44 | * ## Example without an authentication 45 | * 46 | * ``` 47 | * const AdminJS = require('adminjs') 48 | * const AdminJSExpress = require('@adminjs/express') 49 | * 50 | * const express = require('express') 51 | * const app = express() 52 | * 53 | * const adminJs = new AdminJS({ 54 | * databases: [], 55 | * rootPath: '/admin', 56 | * }) 57 | * 58 | * const router = AdminJSExpress.buildRouter(adminJs) 59 | * app.use(adminJs.options.rootPath, router) 60 | * app.listen(8080, () => console.log('AdminJS is running under localhost:8080/admin')) 61 | * ``` 62 | * 63 | * ## Using build in authentication 64 | * 65 | * To protect the routes with a session authentication, you can use predefined 66 | * {@link module:@adminjs/express.buildAuthenticatedRouter} method. 67 | * 68 | * Note! To use authentication in production environment, there is a need to configure 69 | * express-session for production build. It can be achieved by passing options to 70 | * `sessionOptions` parameter. Read more on [express/session Github page](https://github.com/expressjs/session) 71 | * 72 | * ## Adding custom authentication 73 | * 74 | * You can add your custom authentication setup by firstly creating the router and then 75 | * passing it via the `predefinedRouter` option. 76 | * 77 | * ``` 78 | * let router = express.Router() 79 | * router.use((req, res, next) => { 80 | * if (req.session && req.session.admin) { 81 | * req.session.adminUser = req.session.admin 82 | * next() 83 | * } else { 84 | * res.redirect(adminJs.options.loginPath) 85 | * } 86 | * }) 87 | * router = AdminJSExpress.buildRouter(adminJs, router) 88 | * ``` 89 | * 90 | * Where `req.session.admin` is {@link AdminJS#CurrentAdmin}, 91 | * meaning that it should have at least an email property. 92 | */ 93 | 94 | /** 95 | * Plugin name 96 | * @static 97 | * @memberof module:@adminjs/express 98 | */ 99 | export const name = "AdminJSExpressjs"; 100 | export { SessionData } from "express-session"; 101 | 102 | export type ExpressPlugin = { 103 | name: string; 104 | buildAuthenticatedRouter: typeof buildAuthenticatedRouter; 105 | buildRouter: typeof buildRouter; 106 | }; 107 | 108 | const plugin: ExpressPlugin = { name, buildAuthenticatedRouter, buildRouter }; 109 | 110 | export default plugin; 111 | 112 | export type { 113 | AuthenticationOptions, 114 | FormidableOptions, 115 | AuthenticationMaxRetriesOptions, 116 | AuthenticationContext, 117 | } from "./types.js"; 118 | export * from "./buildRouter.js"; 119 | export * from "./buildAuthenticatedRouter.js"; 120 | export * from "./convertRoutes.js"; 121 | export * from "./errors.js"; 122 | export * from "./logger.js"; 123 | export * from "./authentication/login.handler.js"; 124 | export * from "./authentication/logout.handler.js"; 125 | export * from "./authentication/protected-routes.handler.js"; 126 | -------------------------------------------------------------------------------- /src/authentication/login.handler.ts: -------------------------------------------------------------------------------- 1 | import AdminJS from "adminjs"; 2 | import { Router } from "express"; 3 | 4 | import type { 5 | AuthenticationContext, 6 | AuthenticationMaxRetriesOptions, 7 | AuthenticationOptions, 8 | } from "../types.js"; 9 | import { INVALID_AUTH_CONFIG_ERROR, WrongArgumentError } from "../errors.js"; 10 | 11 | const getLoginPath = (admin: AdminJS): string => { 12 | const { loginPath, rootPath } = admin.options; 13 | // since we are inside already namespaced router we have to replace login and logout routes that 14 | // they don't have rootUrl inside. So changing /admin/login to just /login. 15 | // but there is a case where user gives / as a root url and /login becomes `login`. We have to 16 | // fix it by adding / in front of the route 17 | const normalizedLoginPath = loginPath.replace(rootPath, ""); 18 | 19 | return normalizedLoginPath.startsWith("/") 20 | ? normalizedLoginPath 21 | : `/${normalizedLoginPath}`; 22 | }; 23 | 24 | class Retry { 25 | private static retriesContainer: Map = new Map(); 26 | private lastRetry: Date | undefined; 27 | private retriesCount = 0; 28 | 29 | constructor(ip: string) { 30 | const existing = Retry.retriesContainer.get(ip); 31 | if (existing) { 32 | return existing; 33 | } 34 | Retry.retriesContainer.set(ip, this); 35 | } 36 | 37 | public canLogin( 38 | maxRetries: number | AuthenticationMaxRetriesOptions | undefined 39 | ): boolean { 40 | if (maxRetries === undefined) { 41 | return true; 42 | } else if (typeof maxRetries === "number") { 43 | maxRetries = { 44 | count: maxRetries, 45 | duration: 60, 46 | }; 47 | } else if (maxRetries.count <= 0) { 48 | return true; 49 | } 50 | if ( 51 | !this.lastRetry || 52 | new Date().getTime() - this.lastRetry.getTime() > 53 | maxRetries.duration * 1000 54 | ) { 55 | this.lastRetry = new Date(); 56 | this.retriesCount = 1; 57 | return true; 58 | } else { 59 | this.lastRetry = new Date(); 60 | this.retriesCount++; 61 | return this.retriesCount <= maxRetries.count; 62 | } 63 | } 64 | } 65 | 66 | export const withLogin = ( 67 | router: Router, 68 | admin: AdminJS, 69 | auth: AuthenticationOptions 70 | ): void => { 71 | const { rootPath } = admin.options; 72 | const loginPath = getLoginPath(admin); 73 | 74 | const { provider } = auth; 75 | const providerProps = provider?.getUiProps?.() ?? {}; 76 | 77 | router.get(loginPath, async (req, res) => { 78 | const baseProps = { 79 | action: admin.options.loginPath, 80 | errorMessage: null, 81 | }; 82 | const login = await admin.renderLogin({ 83 | ...baseProps, 84 | ...providerProps, 85 | }); 86 | 87 | return res.send(login); 88 | }); 89 | 90 | router.post(loginPath, async (req, res, next) => { 91 | if (!new Retry(req.ip).canLogin(auth.maxRetries)) { 92 | const login = await admin.renderLogin({ 93 | action: admin.options.loginPath, 94 | errorMessage: "tooManyRequests", 95 | ...providerProps, 96 | }); 97 | 98 | return res.send(login); 99 | } 100 | 101 | const context: AuthenticationContext = { req, res }; 102 | 103 | let adminUser; 104 | try { 105 | if (provider) { 106 | adminUser = await provider.handleLogin( 107 | { 108 | headers: req.headers, 109 | query: req.query, 110 | params: req.params, 111 | data: req.fields ?? {}, 112 | }, 113 | context 114 | ); 115 | } else if (auth.authenticate) { 116 | const { email, password } = req.fields as { 117 | email: string; 118 | password: string; 119 | }; 120 | // "auth.authenticate" must always be defined if "auth.provider" isn't 121 | adminUser = await auth.authenticate(email, password, context); 122 | } else { 123 | throw new WrongArgumentError(INVALID_AUTH_CONFIG_ERROR); 124 | } 125 | } catch (error) { 126 | const errorMessage = error.message || error.error || "invalidCredentials"; 127 | 128 | const loginPage = await admin.renderLogin({ 129 | action: admin.options.loginPath, 130 | errorMessage, 131 | ...providerProps, 132 | }); 133 | 134 | return res.status(400).send(loginPage); 135 | } 136 | 137 | if (adminUser) { 138 | req.session.adminUser = adminUser; 139 | req.session.save((err) => { 140 | if (err) { 141 | return next(err); 142 | } 143 | if (req.session.redirectTo) { 144 | return res.redirect(302, req.session.redirectTo); 145 | } else { 146 | return res.redirect(302, rootPath); 147 | } 148 | }); 149 | } else { 150 | const login = await admin.renderLogin({ 151 | action: admin.options.loginPath, 152 | errorMessage: "invalidCredentials", 153 | ...providerProps, 154 | }); 155 | 156 | return res.send(login); 157 | } 158 | }); 159 | }; 160 | --------------------------------------------------------------------------------