From d7b083488c718f3f311c49f4c0a9c095f0a516df Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Wed, 26 Jun 2024 11:06:02 +0200 Subject: [PATCH] enhancement: add a way to register a u&p provider --- .../server/bootstrap/grant-config.js | 140 ---- .../server/bootstrap/index.js | 22 +- .../server/services/providers-registry.js | 770 +++++++++++------- .../users-permissions/server/utils/index.d.ts | 3 +- 4 files changed, 495 insertions(+), 440 deletions(-) delete mode 100644 packages/plugins/users-permissions/server/bootstrap/grant-config.js diff --git a/packages/plugins/users-permissions/server/bootstrap/grant-config.js b/packages/plugins/users-permissions/server/bootstrap/grant-config.js deleted file mode 100644 index 013e92c36a..0000000000 --- a/packages/plugins/users-permissions/server/bootstrap/grant-config.js +++ /dev/null @@ -1,140 +0,0 @@ -'use strict'; - -module.exports = (baseURL) => ({ - email: { - enabled: true, - icon: 'envelope', - }, - discord: { - enabled: false, - icon: 'discord', - key: '', - secret: '', - callback: `${baseURL}/discord/callback`, - scope: ['identify', 'email'], - }, - facebook: { - enabled: false, - icon: 'facebook-square', - key: '', - secret: '', - callback: `${baseURL}/facebook/callback`, - scope: ['email'], - }, - google: { - enabled: false, - icon: 'google', - key: '', - secret: '', - callback: `${baseURL}/google/callback`, - scope: ['email'], - }, - github: { - enabled: false, - icon: 'github', - key: '', - secret: '', - callback: `${baseURL}/github/callback`, - scope: ['user', 'user:email'], - }, - microsoft: { - enabled: false, - icon: 'windows', - key: '', - secret: '', - callback: `${baseURL}/microsoft/callback`, - scope: ['user.read'], - }, - twitter: { - enabled: false, - icon: 'twitter', - key: '', - secret: '', - callback: `${baseURL}/twitter/callback`, - }, - instagram: { - enabled: false, - icon: 'instagram', - key: '', - secret: '', - callback: `${baseURL}/instagram/callback`, - scope: ['user_profile'], - }, - vk: { - enabled: false, - icon: 'vk', - key: '', - secret: '', - callback: `${baseURL}/vk/callback`, - scope: ['email'], - }, - twitch: { - enabled: false, - icon: 'twitch', - key: '', - secret: '', - callback: `${baseURL}/twitch/callback`, - scope: ['user:read:email'], - }, - linkedin: { - enabled: false, - icon: 'linkedin', - key: '', - secret: '', - callback: `${baseURL}/linkedin/callback`, - scope: ['r_liteprofile', 'r_emailaddress'], - }, - cognito: { - enabled: false, - icon: 'aws', - key: '', - secret: '', - subdomain: 'my.subdomain.com', - callback: `${baseURL}/cognito/callback`, - scope: ['email', 'openid', 'profile'], - }, - reddit: { - enabled: false, - icon: 'reddit', - key: '', - secret: '', - state: true, - callback: `${baseURL}/reddit/callback`, - scope: ['identity'], - }, - auth0: { - enabled: false, - icon: '', - key: '', - secret: '', - subdomain: 'my-tenant.eu', - callback: `${baseURL}/auth0/callback`, - scope: ['openid', 'email', 'profile'], - }, - cas: { - enabled: false, - icon: 'book', - key: '', - secret: '', - callback: `${baseURL}/cas/callback`, - scope: ['openid email'], // scopes should be space delimited - subdomain: 'my.subdomain.com/cas', - }, - patreon: { - enabled: false, - icon: '', - key: '', - secret: '', - callback: `${baseURL}/patreon/callback`, - scope: ['identity', 'identity[email]'], - }, - keycloak: { - enabled: false, - icon: '', - key: '', - secret: '', - subdomain: 'myKeycloakProvider.com/realms/myrealm', - callback: `${baseURL}/keycloak/callback`, - scope: ['openid', 'email', 'profile'], - }, -}); diff --git a/packages/plugins/users-permissions/server/bootstrap/index.js b/packages/plugins/users-permissions/server/bootstrap/index.js index 3f29094b6c..33d6bdf634 100644 --- a/packages/plugins/users-permissions/server/bootstrap/index.js +++ b/packages/plugins/users-permissions/server/bootstrap/index.js @@ -9,22 +9,26 @@ */ const crypto = require('crypto'); const _ = require('lodash'); -const urljoin = require('url-join'); const { getService } = require('../utils'); -const getGrantConfig = require('./grant-config'); const usersPermissionsActions = require('./users-permissions-actions'); const initGrant = async (pluginStore) => { - const apiPrefix = strapi.config.get('api.rest.prefix'); - const baseURL = urljoin(strapi.config.server.url, apiPrefix, 'auth'); + const allProviders = getService('providers-registry').getAll(); - const grantConfig = getGrantConfig(baseURL); + const grantConfig = Object.entries(allProviders).reduce((acc, [name, provider]) => { + const { icon, enabled, grantConfig } = provider; + + acc[name] = { + icon, + enabled, + ...grantConfig, + }; + return acc; + }, {}); const prevGrantConfig = (await pluginStore.get({ key: 'grant' })) || {}; - // store grant auth config to db - // when plugin_users-permissions_grant is not existed in db - // or we have added/deleted provider here. - if (!prevGrantConfig || !_.isEqual(_.keys(prevGrantConfig), _.keys(grantConfig))) { + + if (!prevGrantConfig || !_.isEqual(prevGrantConfig, grantConfig)) { // merge with the previous provider config. _.keys(grantConfig).forEach((key) => { if (key in prevGrantConfig) { diff --git a/packages/plugins/users-permissions/server/services/providers-registry.js b/packages/plugins/users-permissions/server/services/providers-registry.js index 855106df69..af6908a451 100644 --- a/packages/plugins/users-permissions/server/services/providers-registry.js +++ b/packages/plugins/users-permissions/server/services/providers-registry.js @@ -2,6 +2,7 @@ const { strict: assert } = require('assert'); const jwt = require('jsonwebtoken'); +const urljoin = require('url-join'); const jwkToPem = require('jwk-to-pem'); const getCognitoPayload = async ({ idToken, jwksUrl, purest }) => { @@ -45,330 +46,519 @@ const getCognitoPayload = async ({ idToken, jwksUrl, purest }) => { } }; -const getInitialProviders = ({ purest }) => ({ - async discord({ accessToken }) { - const discord = purest({ provider: 'discord' }); +const initProviders = ({ baseURL, purest }) => ({ + email: { + enabled: true, + icon: 'envelope', + grantConfig: {}, + }, + discord: { + enabled: false, + icon: 'discord', + grantConfig: { + key: '', + secret: '', + callbackUrl: `${baseURL}/discord/callback`, + scope: ['identify', 'email'], + }, + async authCallback({ accessToken }) { + const discord = purest({ provider: 'discord' }); - return discord - .get('users/@me') - .auth(accessToken) - .request() - .then(({ body }) => { - // Combine username and discriminator because discord username is not unique - const username = `${body.username}#${body.discriminator}`; - return { - username, + return discord + .get('users/@me') + .auth(accessToken) + .request() + .then(({ body }) => { + // Combine username and discriminator because discord username is not unique + const username = `${body.username}#${body.discriminator}`; + return { + username, + email: body.email, + }; + }); + }, + }, + facebook: { + enabled: false, + icon: 'facebook-square', + grantConfig: { + key: '', + secret: '', + callbackUrl: `${baseURL}/facebook/callback`, + scope: ['email'], + }, + async authCallback({ accessToken }) { + const facebook = purest({ provider: 'facebook' }); + + return facebook + .get('me') + .auth(accessToken) + .qs({ fields: 'name,email' }) + .request() + .then(({ body }) => ({ + username: body.name, email: body.email, - }; - }); + })); + }, }, - async cognito({ query, providers }) { - const jwksUrl = new URL(providers.cognito.jwksurl); - const idToken = query.id_token; - const tokenPayload = await getCognitoPayload({ idToken, jwksUrl, purest }); - return { - username: tokenPayload['cognito:username'], - email: tokenPayload.email, - }; - }, - async facebook({ accessToken }) { - const facebook = purest({ provider: 'facebook' }); + google: { + enabled: false, + icon: 'google', + grantConfig: { + key: '', + secret: '', + callbackUrl: `${baseURL}/google/callback`, + scope: ['email'], + }, + async authCallback({ accessToken }) { + const google = purest({ provider: 'google' }); - return facebook - .get('me') - .auth(accessToken) - .qs({ fields: 'name,email' }) - .request() - .then(({ body }) => ({ - username: body.name, - email: body.email, - })); + return google + .query('oauth') + .get('tokeninfo') + .qs({ accessToken }) + .request() + .then(({ body }) => ({ + username: body.email.split('@')[0], + email: body.email, + })); + }, }, - async google({ accessToken }) { - const google = purest({ provider: 'google' }); - - return google - .query('oauth') - .get('tokeninfo') - .qs({ accessToken }) - .request() - .then(({ body }) => ({ - username: body.email.split('@')[0], - email: body.email, - })); - }, - async github({ accessToken }) { - const github = purest({ - provider: 'github', - defaults: { - headers: { - 'user-agent': 'strapi', + github: { + enabled: false, + icon: 'github', + grantConfig: { + key: '', + secret: '', + callbackUrl: `${baseURL}/github/callback`, + scope: ['user', 'user:email'], + }, + async authCallback({ accessToken }) { + const github = purest({ + provider: 'github', + defaults: { + headers: { + 'user-agent': 'strapi', + }, }, - }, - }); + }); - const { body: userBody } = await github.get('user').auth(accessToken).request(); + const { body: userBody } = await github.get('user').auth(accessToken).request(); + + // This is the public email on the github profile + if (userBody.email) { + return { + username: userBody.login, + email: userBody.email, + }; + } + // Get the email with Github's user/emails API + const { body: emailBody } = await github.get('user/emails').auth(accessToken).request(); - // This is the public email on the github profile - if (userBody.email) { return { username: userBody.login, - email: userBody.email, + email: Array.isArray(emailBody) + ? emailBody.find((email) => email.primary === true).email + : null, }; - } - // Get the email with Github's user/emails API - const { body: emailBody } = await github.get('user/emails').auth(accessToken).request(); - - return { - username: userBody.login, - email: Array.isArray(emailBody) - ? emailBody.find((email) => email.primary === true).email - : null, - }; + }, }, - async microsoft({ accessToken }) { - const microsoft = purest({ provider: 'microsoft' }); + microsoft: { + enabled: false, + icon: 'windows', + grantConfig: { + key: '', + secret: '', + callbackUrl: `${baseURL}/microsoft/callback`, + scope: ['user.read'], + }, + async authCallback({ accessToken }) { + const microsoft = purest({ provider: 'microsoft' }); - return microsoft - .get('me') - .auth(accessToken) - .request() - .then(({ body }) => ({ - username: body.userPrincipalName, - email: body.userPrincipalName, - })); + return microsoft + .get('me') + .auth(accessToken) + .request() + .then(({ body }) => ({ + username: body.userPrincipalName, + email: body.userPrincipalName, + })); + }, }, - async twitter({ accessToken, query, providers }) { - const twitter = purest({ - provider: 'twitter', - defaults: { - oauth: { - consumer_key: providers.twitter.key, - consumer_secret: providers.twitter.secret, - }, - }, - }); - return twitter - .get('account/verify_credentials') - .auth(accessToken, query.access_secret) - .qs({ screen_name: query['raw[screen_name]'], include_email: 'true' }) - .request() - .then(({ body }) => ({ - username: body.screen_name, - email: body.email, - })); - }, - async instagram({ accessToken }) { - const instagram = purest({ provider: 'instagram' }); - - return instagram - .get('me') - .auth(accessToken) - .qs({ fields: 'id,username' }) - .request() - .then(({ body }) => ({ - username: body.username, - email: `${body.username}@strapi.io`, // dummy email as Instagram does not provide user email - })); - }, - async vk({ accessToken, query }) { - const vk = purest({ provider: 'vk' }); - - return vk - .get('users') - .auth(accessToken) - .qs({ id: query.raw.user_id, v: '5.122' }) - .request() - .then(({ body }) => ({ - username: `${body.response[0].last_name} ${body.response[0].first_name}`, - email: query.raw.email, - })); - }, - async twitch({ accessToken, providers }) { - const twitch = purest({ - provider: 'twitch', - config: { - twitch: { - default: { - origin: 'https://api.twitch.tv', - path: 'helix/{path}', - headers: { - Authorization: 'Bearer {auth}', - 'Client-Id': '{auth}', - }, + twitter: { + enabled: false, + icon: 'twitter', + grantConfig: { + key: '', + secret: '', + callbackUrl: `${baseURL}/twitter/callback`, + }, + async authCallback({ accessToken, query, providers }) { + const twitter = purest({ + provider: 'twitter', + defaults: { + oauth: { + consumer_key: providers.twitter.key, + consumer_secret: providers.twitter.secret, }, }, - }, - }); - - return twitch - .get('users') - .auth(accessToken, providers.twitch.key) - .request() - .then(({ body }) => ({ - username: body.data[0].login, - email: body.data[0].email, - })); - }, - async linkedin({ accessToken }) { - const linkedIn = purest({ provider: 'linkedin' }); - const { - body: { localizedFirstName }, - } = await linkedIn.get('me').auth(accessToken).request(); - const { - body: { elements }, - } = await linkedIn - .get('emailAddress?q=members&projection=(elements*(handle~))') - .auth(accessToken) - .request(); - - const email = elements[0]['handle~']; - - return { - username: localizedFirstName, - email: email.emailAddress, - }; - }, - async reddit({ accessToken }) { - const reddit = purest({ - provider: 'reddit', - config: { - reddit: { - default: { - origin: 'https://oauth.reddit.com', - path: 'api/{version}/{path}', - version: 'v1', - headers: { - Authorization: 'Bearer {auth}', - 'user-agent': 'strapi', - }, - }, - }, - }, - }); - - return reddit - .get('me') - .auth(accessToken) - .request() - .then(({ body }) => ({ - username: body.name, - email: `${body.name}@strapi.io`, // dummy email as Reddit does not provide user email - })); - }, - async auth0({ accessToken, providers }) { - const auth0 = purest({ provider: 'auth0' }); - - return auth0 - .get('userinfo') - .subdomain(providers.auth0.subdomain) - .auth(accessToken) - .request() - .then(({ body }) => { - const username = body.username || body.nickname || body.name || body.email.split('@')[0]; - const email = body.email || `${username.replace(/\s+/g, '.')}@strapi.io`; - - return { - username, - email, - }; }); - }, - async cas({ accessToken, providers }) { - const cas = purest({ provider: 'cas' }); - return cas - .get('oidc/profile') - .subdomain(providers.cas.subdomain) - .auth(accessToken) - .request() - .then(({ body }) => { - // CAS attribute may be in body.attributes or "FLAT", depending on CAS config - const username = body.attributes - ? body.attributes.strapiusername || body.id || body.sub - : body.strapiusername || body.id || body.sub; - const email = body.attributes - ? body.attributes.strapiemail || body.attributes.email - : body.strapiemail || body.email; - if (!username || !email) { - strapi.log.warn( - `CAS Response Body did not contain required attributes: ${JSON.stringify(body)}` - ); - } - return { - username, - email, - }; - }); - }, - async patreon({ accessToken }) { - const patreon = purest({ - provider: 'patreon', - config: { - patreon: { - default: { - origin: 'https://www.patreon.com', - path: 'api/oauth2/{path}', - headers: { - authorization: 'Bearer {auth}', - }, - }, - }, - }, - }); - - return patreon - .get('v2/identity') - .auth(accessToken) - .qs(new URLSearchParams({ 'fields[user]': 'full_name,email' }).toString()) - .request() - .then(({ body }) => { - const patreonData = body.data.attributes; - return { - username: patreonData.full_name, - email: patreonData.email, - }; - }); - }, - async keycloak({ accessToken, providers }) { - const keycloak = purest({ provider: 'keycloak' }); - - return keycloak - .subdomain(providers.keycloak.subdomain) - .get('protocol/openid-connect/userinfo') - .auth(accessToken) - .request() - .then(({ body }) => { - return { - username: body.preferred_username, + return twitter + .get('account/verify_credentials') + .auth(accessToken, query.access_secret) + .qs({ screen_name: query['raw[screen_name]'], include_email: 'true' }) + .request() + .then(({ body }) => ({ + username: body.screen_name, email: body.email, - }; + })); + }, + }, + instagram: { + enabled: false, + icon: 'instagram', + grantConfig: { + key: '', + secret: '', + callbackUrl: `${baseURL}/instagram/callback`, + scope: ['user_profile'], + }, + async authCallback({ accessToken }) { + const instagram = purest({ provider: 'instagram' }); + + return instagram + .get('me') + .auth(accessToken) + .qs({ fields: 'id,username' }) + .request() + .then(({ body }) => ({ + username: body.username, + email: `${body.username}@strapi.io`, // dummy email as Instagram does not provide user email + })); + }, + }, + vk: { + enabled: false, + icon: 'vk', + grantConfig: { + key: '', + secret: '', + callbackUrl: `${baseURL}/vk/callback`, + scope: ['email'], + }, + async authCallback({ accessToken, query }) { + const vk = purest({ provider: 'vk' }); + + return vk + .get('users') + .auth(accessToken) + .qs({ id: query.raw.user_id, v: '5.122' }) + .request() + .then(({ body }) => ({ + username: `${body.response[0].last_name} ${body.response[0].first_name}`, + email: query.raw.email, + })); + }, + }, + + twitch: { + enabled: false, + icon: 'twitch', + grantConfig: { + key: '', + secret: '', + callbackUrl: `${baseURL}/twitch/callback`, + scope: ['user:read:email'], + }, + async authCallback({ accessToken, providers }) { + const twitch = purest({ + provider: 'twitch', + config: { + twitch: { + default: { + origin: 'https://api.twitch.tv', + path: 'helix/{path}', + headers: { + Authorization: 'Bearer {auth}', + 'Client-Id': '{auth}', + }, + }, + }, + }, }); + + return twitch + .get('users') + .auth(accessToken, providers.twitch.key) + .request() + .then(({ body }) => ({ + username: body.data[0].login, + email: body.data[0].email, + })); + }, + }, + + linkedin: { + enabled: false, + icon: 'linkedin', + grantConfig: { + key: '', + secret: '', + callbackUrl: `${baseURL}/linkedin/callback`, + scope: ['r_liteprofile', 'r_emailaddress'], + }, + async authCallback({ accessToken }) { + const linkedIn = purest({ provider: 'linkedin' }); + const { + body: { localizedFirstName }, + } = await linkedIn.get('me').auth(accessToken).request(); + const { + body: { elements }, + } = await linkedIn + .get('emailAddress?q=members&projection=(elements*(handle~))') + .auth(accessToken) + .request(); + + const email = elements[0]['handle~']; + + return { + username: localizedFirstName, + email: email.emailAddress, + }; + }, + }, + + cognito: { + enabled: false, + icon: 'aws', + grantConfig: { + key: '', + secret: '', + subdomain: 'my.subdomain.com', + callback: `${baseURL}/cognito/callback`, + scope: ['email', 'openid', 'profile'], + }, + async authCallback({ query, providers }) { + const jwksUrl = new URL(providers.cognito.jwksurl); + const idToken = query.id_token; + const tokenPayload = await getCognitoPayload({ idToken, jwksUrl, purest }); + return { + username: tokenPayload['cognito:username'], + email: tokenPayload.email, + }; + }, + }, + + reddit: { + enabled: false, + icon: 'reddit', + grantConfig: { + key: '', + secret: '', + callback: `${baseURL}/reddit/callback`, + scope: ['identity'], + }, + async authCallback({ accessToken }) { + const reddit = purest({ + provider: 'reddit', + config: { + reddit: { + default: { + origin: 'https://oauth.reddit.com', + path: 'api/{version}/{path}', + version: 'v1', + headers: { + Authorization: 'Bearer {auth}', + 'user-agent': 'strapi', + }, + }, + }, + }, + }); + + return reddit + .get('me') + .auth(accessToken) + .request() + .then(({ body }) => ({ + username: body.name, + email: `${body.name}@strapi.io`, // dummy email as Reddit does not provide user email + })); + }, + }, + + auth0: { + enabled: false, + icon: '', + grantConfig: { + key: '', + secret: '', + subdomain: 'my-tenant.eu', + callback: `${baseURL}/auth0/callback`, + scope: ['openid', 'email', 'profile'], + }, + async authCallback({ accessToken, providers }) { + const auth0 = purest({ provider: 'auth0' }); + + return auth0 + .get('userinfo') + .subdomain(providers.auth0.subdomain) + .auth(accessToken) + .request() + .then(({ body }) => { + const username = body.username || body.nickname || body.name || body.email.split('@')[0]; + const email = body.email || `${username.replace(/\s+/g, '.')}@strapi.io`; + + return { + username, + email, + }; + }); + }, + }, + + cas: { + enabled: false, + icon: 'book', + grantConfig: { + key: '', + secret: '', + callback: `${baseURL}/cas/callback`, + scope: ['openid email'], // scopes should be space delimited + subdomain: 'my.subdomain.com/cas', + }, + async authCallback({ accessToken, providers }) { + const cas = purest({ provider: 'cas' }); + + return cas + .get('oidc/profile') + .subdomain(providers.cas.subdomain) + .auth(accessToken) + .request() + .then(({ body }) => { + // CAS attribute may be in body.attributes or "FLAT", depending on CAS config + const username = body.attributes + ? body.attributes.strapiusername || body.id || body.sub + : body.strapiusername || body.id || body.sub; + const email = body.attributes + ? body.attributes.strapiemail || body.attributes.email + : body.strapiemail || body.email; + if (!username || !email) { + strapi.log.warn( + `CAS Response Body did not contain required attributes: ${JSON.stringify(body)}` + ); + } + return { + username, + email, + }; + }); + }, + }, + + patreon: { + enabled: false, + icon: '', + grantConfig: { + key: '', + secret: '', + callback: `${baseURL}/patreon/callback`, + scope: ['identity', 'identity[email]'], + }, + async authCallback({ accessToken }) { + const patreon = purest({ + provider: 'patreon', + config: { + patreon: { + default: { + origin: 'https://www.patreon.com', + path: 'api/oauth2/{path}', + headers: { + authorization: 'Bearer {auth}', + }, + }, + }, + }, + }); + + return patreon + .get('v2/identity') + .auth(accessToken) + .qs(new URLSearchParams({ 'fields[user]': 'full_name,email' }).toString()) + .request() + .then(({ body }) => { + const patreonData = body.data.attributes; + return { + username: patreonData.full_name, + email: patreonData.email, + }; + }); + }, + }, + keycloack: { + enabled: false, + icon: '', + grantConfig: { + key: '', + secret: '', + subdomain: 'myKeycloakProvider.com/realms/myrealm', + callback: `${baseURL}/keycloak/callback`, + scope: ['openid', 'email', 'profile'], + }, + async authCallback({ accessToken, providers }) { + const keycloak = purest({ provider: 'keycloak' }); + + return keycloak + .subdomain(providers.keycloak.subdomain) + .get('protocol/openid-connect/userinfo') + .auth(accessToken) + .request() + .then(({ body }) => { + return { + username: body.preferred_username, + email: body.email, + }; + }); + }, }, }); module.exports = () => { const purest = require('purest'); - const providersCallbacks = getInitialProviders({ purest }); + const apiPrefix = strapi.config.get('api.rest.prefix'); + const baseURL = urljoin(strapi.config.server.url, apiPrefix, 'auth'); + const authProviders = initProviders({ baseURL, purest }); + + /** + * @public + */ return { - register(providerName, provider) { - assert(typeof providerName === 'string', 'Provider name must be a string'); - assert(typeof provider === 'function', 'Provider callback must be a function'); - - providersCallbacks[providerName] = provider({ purest }); + getAll() { + return authProviders; + }, + get(name) { + return authProviders[name]; + }, + add(name, config) { + authProviders[name] = config; + }, + remove(name) { + delete authProviders[name]; }, + /** + * @internal + */ async run({ provider, accessToken, query, providers }) { - if (!providersCallbacks[provider]) { - throw new Error('Unknown provider.'); - } + const authProvider = authProviders[provider]; - const providerCb = providersCallbacks[provider]; + assert(authProvider, 'Unknown auth provider'); - return providerCb({ accessToken, query, providers }); + return authProvider.authCallback({ accessToken, query, providers, purest }); }, }; }; diff --git a/packages/plugins/users-permissions/server/utils/index.d.ts b/packages/plugins/users-permissions/server/utils/index.d.ts index b9d7e20883..88f3c70e2e 100644 --- a/packages/plugins/users-permissions/server/utils/index.d.ts +++ b/packages/plugins/users-permissions/server/utils/index.d.ts @@ -3,6 +3,7 @@ import * as user from '../services/user'; import * as role from '../services/role'; import * as jwt from '../services/jwt'; import * as providers from '../services/providers'; +import * as providersRegistry from '../services/providers-registry'; import * as permission from '../services/permission'; type S = { @@ -11,7 +12,7 @@ type S = { user: typeof user; jwt: typeof jwt; providers: typeof providers; - ['providers-registry']: typeof providers; + ['providers-registry']: typeof providersRegistry; permission: typeof permission; };