@strapi/plugin-users-permissions 4.3.0-beta.1 → 4.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
2
4
 
3
5
  const authStrategy = require('./strategies/users-permissions');
4
6
  const sanitizers = require('./utils/sanitize/sanitizers');
@@ -10,4 +12,14 @@ module.exports = ({ strapi }) => {
10
12
  if (strapi.plugin('graphql')) {
11
13
  require('./graphql')({ strapi });
12
14
  }
15
+
16
+ if (strapi.plugin('documentation')) {
17
+ const specPath = path.join(__dirname, '../documentation/content-api.yaml');
18
+ const spec = fs.readFileSync(specPath, 'utf8');
19
+
20
+ strapi
21
+ .plugin('documentation')
22
+ .service('documentation')
23
+ .registerDoc(spec);
24
+ }
13
25
  };
@@ -14,7 +14,6 @@ module.exports = [
14
14
  path: '/users',
15
15
  handler: 'user.find',
16
16
  config: {
17
- auth: {},
18
17
  prefix: '',
19
18
  },
20
19
  },
@@ -5,10 +5,12 @@ const providers = require('./providers');
5
5
  const user = require('./user');
6
6
  const role = require('./role');
7
7
  const usersPermissions = require('./users-permissions');
8
+ const providersRegistry = require('./providers-registry');
8
9
 
9
10
  module.exports = {
10
11
  jwt,
11
12
  providers,
13
+ 'providers-registry': providersRegistry,
12
14
  role,
13
15
  user,
14
16
  'users-permissions': usersPermissions,
@@ -0,0 +1,301 @@
1
+ 'use strict';
2
+
3
+ const { strict: assert } = require('assert');
4
+ const jwt = require('jsonwebtoken');
5
+
6
+ const getInitialProviders = ({ purest }) => ({
7
+ async discord({ access_token }) {
8
+ const discord = purest({ provider: 'discord' });
9
+ return discord
10
+ .get('users/@me')
11
+ .auth(access_token)
12
+ .request()
13
+ .then(({ body }) => {
14
+ // Combine username and discriminator because discord username is not unique
15
+ var username = `${body.username}#${body.discriminator}`;
16
+ return {
17
+ username,
18
+ email: body.email,
19
+ };
20
+ });
21
+ },
22
+ async cognito({ query }) {
23
+ // get the id_token
24
+ const idToken = query.id_token;
25
+ // decode the jwt token
26
+ const tokenPayload = jwt.decode(idToken);
27
+ if (!tokenPayload) {
28
+ throw new Error('unable to decode jwt token');
29
+ } else {
30
+ return {
31
+ username: tokenPayload['cognito:username'],
32
+ email: tokenPayload.email,
33
+ };
34
+ }
35
+ },
36
+ async facebook({ access_token }) {
37
+ const facebook = purest({ provider: 'facebook' });
38
+
39
+ return facebook
40
+ .get('me')
41
+ .auth(access_token)
42
+ .qs({ fields: 'name,email' })
43
+ .request()
44
+ .then(({ body }) => ({
45
+ username: body.name,
46
+ email: body.email,
47
+ }));
48
+ },
49
+ async google({ access_token }) {
50
+ const google = purest({ provider: 'google' });
51
+
52
+ return google
53
+ .query('oauth')
54
+ .get('tokeninfo')
55
+ .qs({ access_token })
56
+ .request()
57
+ .then(({ body }) => ({
58
+ username: body.email.split('@')[0],
59
+ email: body.email,
60
+ }));
61
+ },
62
+ async github({ access_token }) {
63
+ const github = purest({
64
+ provider: 'github',
65
+ defaults: {
66
+ headers: {
67
+ 'user-agent': 'strapi',
68
+ },
69
+ },
70
+ });
71
+
72
+ const { body: userBody } = await github
73
+ .get('user')
74
+ .auth(access_token)
75
+ .request();
76
+
77
+ // This is the public email on the github profile
78
+ if (userBody.email) {
79
+ return {
80
+ username: userBody.login,
81
+ email: userBody.email,
82
+ };
83
+ }
84
+ // Get the email with Github's user/emails API
85
+ const { body: emailBody } = await github
86
+ .get('user/emails')
87
+ .auth(access_token)
88
+ .request();
89
+
90
+ return {
91
+ username: userBody.login,
92
+ email: Array.isArray(emailBody)
93
+ ? emailBody.find(email => email.primary === true).email
94
+ : null,
95
+ };
96
+ },
97
+ async microsoft({ access_token }) {
98
+ const microsoft = purest({ provider: 'microsoft' });
99
+
100
+ return microsoft
101
+ .get('me')
102
+ .auth(access_token)
103
+ .request()
104
+ .then(({ body }) => ({
105
+ username: body.userPrincipalName,
106
+ email: body.userPrincipalName,
107
+ }));
108
+ },
109
+ async twitter({ access_token, query, providers }) {
110
+ const twitter = purest({
111
+ provider: 'twitter',
112
+ defaults: {
113
+ oauth: {
114
+ consumer_key: providers.twitter.key,
115
+ consumer_secret: providers.twitter.secret,
116
+ },
117
+ },
118
+ });
119
+
120
+ return twitter
121
+ .get('account/verify_credentials')
122
+ .auth(access_token, query.access_secret)
123
+ .qs({ screen_name: query['raw[screen_name]'], include_email: 'true' })
124
+ .request()
125
+ .then(({ body }) => ({
126
+ username: body.screen_name,
127
+ email: body.email,
128
+ }));
129
+ },
130
+ async instagram({ access_token }) {
131
+ const instagram = purest({ provider: 'instagram' });
132
+
133
+ return instagram
134
+ .get('me')
135
+ .auth(access_token)
136
+ .qs({ fields: 'id,username' })
137
+ .request()
138
+ .then(({ body }) => ({
139
+ username: body.username,
140
+ email: `${body.username}@strapi.io`, // dummy email as Instagram does not provide user email
141
+ }));
142
+ },
143
+ async vk({ access_token, query }) {
144
+ const vk = purest({ provider: 'vk' });
145
+
146
+ return vk
147
+ .get('users.get')
148
+ .auth(access_token)
149
+ .qs({ id: query.raw.user_id, v: '5.122' })
150
+ .request()
151
+ .then(({ body }) => ({
152
+ username: `${body.response[0].last_name} ${body.response[0].first_name}`,
153
+ email: query.raw.email,
154
+ }));
155
+ },
156
+ async twitch({ access_token, providers }) {
157
+ const twitch = purest({
158
+ provider: 'twitch',
159
+ config: {
160
+ twitch: {
161
+ default: {
162
+ origin: 'https://api.twitch.tv',
163
+ path: 'helix/{path}',
164
+ headers: {
165
+ Authorization: 'Bearer {auth}',
166
+ 'Client-Id': '{auth}',
167
+ },
168
+ },
169
+ },
170
+ },
171
+ });
172
+
173
+ return twitch
174
+ .get('users')
175
+ .auth(access_token, providers.twitch.key)
176
+ .request()
177
+ .then(({ body }) => ({
178
+ username: body.data[0].login,
179
+ email: body.data[0].email,
180
+ }));
181
+ },
182
+ async linkedin({ access_token }) {
183
+ const linkedIn = purest({ provider: 'linkedin' });
184
+ const {
185
+ body: { localizedFirstName },
186
+ } = await linkedIn
187
+ .get('me')
188
+ .auth(access_token)
189
+ .request();
190
+ const {
191
+ body: { elements },
192
+ } = await linkedIn
193
+ .get('emailAddress?q=members&projection=(elements*(handle~))')
194
+ .auth(access_token)
195
+ .request();
196
+
197
+ const email = elements[0]['handle~'];
198
+
199
+ return {
200
+ username: localizedFirstName,
201
+ email: email.emailAddress,
202
+ };
203
+ },
204
+ async reddit({ access_token }) {
205
+ const reddit = purest({
206
+ provider: 'reddit',
207
+ config: {
208
+ reddit: {
209
+ default: {
210
+ origin: 'https://oauth.reddit.com',
211
+ path: 'api/{version}/{path}',
212
+ version: 'v1',
213
+ headers: {
214
+ Authorization: 'Bearer {auth}',
215
+ 'user-agent': 'strapi',
216
+ },
217
+ },
218
+ },
219
+ },
220
+ });
221
+
222
+ return reddit
223
+ .get('me')
224
+ .auth(access_token)
225
+ .request()
226
+ .then(({ body }) => ({
227
+ username: body.name,
228
+ email: `${body.name}@strapi.io`, // dummy email as Reddit does not provide user email
229
+ }));
230
+ },
231
+ async auth0({ access_token, providers }) {
232
+ const auth0 = purest({ provider: 'auth0' });
233
+
234
+ return auth0
235
+ .get('userinfo')
236
+ .subdomain(providers.auth0.subdomain)
237
+ .auth(access_token)
238
+ .request()
239
+ .then(({ body }) => {
240
+ const username = body.username || body.nickname || body.name || body.email.split('@')[0];
241
+ const email = body.email || `${username.replace(/\s+/g, '.')}@strapi.io`;
242
+
243
+ return {
244
+ username,
245
+ email,
246
+ };
247
+ });
248
+ },
249
+ async cas({ access_token, providers }) {
250
+ const cas = purest({ provider: 'cas' });
251
+
252
+ return cas
253
+ .get('oidc/profile')
254
+ .subdomain(providers.cas.subdomain)
255
+ .auth(access_token)
256
+ .request()
257
+ .then(({ body }) => {
258
+ // CAS attribute may be in body.attributes or "FLAT", depending on CAS config
259
+ const username = body.attributes
260
+ ? body.attributes.strapiusername || body.id || body.sub
261
+ : body.strapiusername || body.id || body.sub;
262
+ const email = body.attributes
263
+ ? body.attributes.strapiemail || body.attributes.email
264
+ : body.strapiemail || body.email;
265
+ if (!username || !email) {
266
+ strapi.log.warn(
267
+ 'CAS Response Body did not contain required attributes: ' + JSON.stringify(body)
268
+ );
269
+ }
270
+ return {
271
+ username,
272
+ email,
273
+ };
274
+ });
275
+ },
276
+ });
277
+
278
+ module.exports = () => {
279
+ const purest = require('purest');
280
+
281
+ const providersCallbacks = getInitialProviders({ purest });
282
+
283
+ return {
284
+ register(providerName, provider) {
285
+ assert(typeof providerName === 'string', 'Provider name must be a string');
286
+ assert(typeof provider === 'function', 'Provider callback must be a function');
287
+
288
+ providersCallbacks[providerName] = provider({ purest });
289
+ },
290
+
291
+ async run({ provider, access_token, query, providers }) {
292
+ if (!providersCallbacks[provider]) {
293
+ throw new Error('Unknown provider.');
294
+ }
295
+
296
+ const providerCb = providersCallbacks[provider];
297
+
298
+ return providerCb({ access_token, query, providers });
299
+ },
300
+ };
301
+ };
@@ -9,11 +9,9 @@ const _ = require('lodash');
9
9
  const urlJoin = require('url-join');
10
10
 
11
11
  const { getAbsoluteServerUrl } = require('@strapi/utils');
12
+ const { getService } = require('../utils');
12
13
 
13
14
  module.exports = ({ strapi }) => {
14
- // lazy load heavy dependencies
15
- const providerRequest = require('./providers-list');
16
-
17
15
  /**
18
16
  * Helper to get profiles
19
17
  *
@@ -27,7 +25,12 @@ module.exports = ({ strapi }) => {
27
25
  .store({ type: 'plugin', name: 'users-permissions', key: 'grant' })
28
26
  .get();
29
27
 
30
- return providerRequest({ provider, query, access_token, providers });
28
+ return getService('providers-registry').run({
29
+ provider,
30
+ query,
31
+ access_token,
32
+ providers,
33
+ });
31
34
  };
32
35
 
33
36
  /**
@@ -40,75 +43,64 @@ module.exports = ({ strapi }) => {
40
43
  * @return {*}
41
44
  */
42
45
 
43
- const connect = (provider, query) => {
46
+ const connect = async (provider, query) => {
44
47
  const access_token = query.access_token || query.code || query.oauth_token;
45
48
 
46
- return new Promise((resolve, reject) => {
47
- if (!access_token) {
48
- return reject({ message: 'No access_token.' });
49
- }
50
-
51
- // Get the profile.
52
- getProfile(provider, query)
53
- .then(async profile => {
54
- const email = _.toLower(profile.email);
55
-
56
- // We need at least the mail.
57
- if (!email) {
58
- return reject({ message: 'Email was not available.' });
59
- }
60
-
61
- try {
62
- const users = await strapi.query('plugin::users-permissions.user').findMany({
63
- where: { email },
64
- });
65
-
66
- const advanced = await strapi
67
- .store({ type: 'plugin', name: 'users-permissions', key: 'advanced' })
68
- .get();
69
-
70
- const user = _.find(users, { provider });
71
-
72
- if (_.isEmpty(user) && !advanced.allow_register) {
73
- return reject({ message: 'Register action is actually not available.' });
74
- }
75
-
76
- if (!_.isEmpty(user)) {
77
- return resolve(user);
78
- }
79
-
80
- if (
81
- !_.isEmpty(_.find(users, user => user.provider !== provider)) &&
82
- advanced.unique_email
83
- ) {
84
- return reject({ message: 'Email is already taken.' });
85
- }
86
-
87
- // Retrieve default role.
88
- const defaultRole = await strapi
89
- .query('plugin::users-permissions.role')
90
- .findOne({ where: { type: advanced.default_role } });
91
-
92
- // Create the new user.
93
- const params = {
94
- ...profile,
95
- email, // overwrite with lowercased email
96
- provider,
97
- role: defaultRole.id,
98
- confirmed: true,
99
- };
100
-
101
- const createdUser = await strapi
102
- .query('plugin::users-permissions.user')
103
- .create({ data: params });
104
-
105
- return resolve(createdUser);
106
- } catch (err) {
107
- reject(err);
108
- }
109
- })
110
- .catch(reject);
49
+ if (!access_token) {
50
+ throw new Error('No access_token.');
51
+ }
52
+
53
+ // Get the profile.
54
+ const profile = await getProfile(provider, query);
55
+
56
+ const email = _.toLower(profile.email);
57
+
58
+ // We need at least the mail.
59
+ if (!email) {
60
+ throw new Error('Email was not available.');
61
+ }
62
+
63
+ const users = await strapi.query('plugin::users-permissions.user').findMany({
64
+ where: { email },
111
65
  });
66
+
67
+ const advancedSettings = await strapi
68
+ .store({ type: 'plugin', name: 'users-permissions', key: 'advanced' })
69
+ .get();
70
+
71
+ const user = _.find(users, { provider });
72
+
73
+ if (_.isEmpty(user) && !advancedSettings.allow_register) {
74
+ throw new Error('Register action is actually not available.');
75
+ }
76
+
77
+ if (!_.isEmpty(user)) {
78
+ return user;
79
+ }
80
+
81
+ if (users.length > 1 && advancedSettings.unique_email) {
82
+ throw new Error('Email is already taken.');
83
+ }
84
+
85
+ // Retrieve default role.
86
+ const defaultRole = await strapi
87
+ .query('plugin::users-permissions.role')
88
+ .findOne({ where: { type: advancedSettings.default_role } });
89
+
90
+ // Create the new user.
91
+ const newUser = {
92
+ ...profile,
93
+ email, // overwrite with lowercased email
94
+ provider,
95
+ role: defaultRole.id,
96
+ confirmed: true,
97
+ };
98
+
99
+ const createdUser = await strapi
100
+ .query('plugin::users-permissions.user')
101
+ .create({ data: newUser });
102
+
103
+ return createdUser;
112
104
  };
113
105
 
114
106
  const buildRedirectUri = (provider = '') => {
@@ -7,15 +7,14 @@ const urlJoin = require('url-join');
7
7
  const { getService } = require('../utils');
8
8
 
9
9
  const DEFAULT_PERMISSIONS = [
10
- { action: 'plugin::users-permissions.auth.admincallback', roleType: 'public' },
11
- { action: 'plugin::users-permissions.auth.adminregister', roleType: 'public' },
12
10
  { action: 'plugin::users-permissions.auth.callback', roleType: 'public' },
13
- { action: 'plugin::users-permissions.auth.connect', roleType: null },
14
- { action: 'plugin::users-permissions.auth.forgotpassword', roleType: 'public' },
15
- { action: 'plugin::users-permissions.auth.resetpassword', roleType: 'public' },
11
+ { action: 'plugin::users-permissions.auth.connect', roleType: 'public' },
12
+ { action: 'plugin::users-permissions.auth.forgotPassword', roleType: 'public' },
13
+ { action: 'plugin::users-permissions.auth.resetPassword', roleType: 'public' },
16
14
  { action: 'plugin::users-permissions.auth.register', roleType: 'public' },
17
- { action: 'plugin::users-permissions.auth.emailconfirmation', roleType: 'public' },
18
- { action: 'plugin::users-permissions.user.me', roleType: null },
15
+ { action: 'plugin::users-permissions.auth.emailConfirmation', roleType: 'public' },
16
+ { action: 'plugin::users-permissions.auth.sendEmailConfirmation', roleType: 'public' },
17
+ { action: 'plugin::users-permissions.user.me', roleType: 'authenticated' },
19
18
  ];
20
19
 
21
20
  const transformRoutePrefixFor = pluginName => route => {
@@ -4,13 +4,13 @@ import * as role from '../services/role';
4
4
  import * as jwt from '../services/jwt';
5
5
  import * as providers from '../services/providers';
6
6
 
7
-
8
7
  type S = {
9
8
  ['users-permissions']: typeof usersPermissions;
10
9
  ['role']: typeof role;
11
10
  user: typeof user;
12
11
  jwt: typeof jwt;
13
12
  providers: typeof providers;
13
+ ['providers-registry']: typeof providers;
14
14
  };
15
15
 
16
16
  export function getService<T extends keyof S>(name: T): ReturnType<S[T]>;