@strapi/plugin-users-permissions 5.49.0 → 5.50.0

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.
Files changed (59) hide show
  1. package/.eslintrc.cjs +1 -0
  2. package/admin/src/pages/AdvancedSettings/index.jsx +1 -1
  3. package/admin/src/pages/Roles/pages/ListPage/index.jsx +1 -1
  4. package/admin/src/translations/ja.json +46 -7
  5. package/dist/admin/pages/AdvancedSettings/index.js +1 -1
  6. package/dist/admin/pages/AdvancedSettings/index.js.map +1 -1
  7. package/dist/admin/pages/AdvancedSettings/index.mjs +1 -1
  8. package/dist/admin/pages/AdvancedSettings/index.mjs.map +1 -1
  9. package/dist/admin/pages/Roles/pages/ListPage/index.js +1 -1
  10. package/dist/admin/pages/Roles/pages/ListPage/index.js.map +1 -1
  11. package/dist/admin/pages/Roles/pages/ListPage/index.mjs +1 -1
  12. package/dist/admin/pages/Roles/pages/ListPage/index.mjs.map +1 -1
  13. package/dist/admin/translations/ja.json.js +46 -7
  14. package/dist/admin/translations/ja.json.js.map +1 -1
  15. package/dist/admin/translations/ja.json.mjs +46 -7
  16. package/dist/admin/translations/ja.json.mjs.map +1 -1
  17. package/dist/server/controllers/auth.js +133 -114
  18. package/dist/server/controllers/auth.js.map +1 -1
  19. package/dist/server/controllers/auth.mjs +132 -113
  20. package/dist/server/controllers/auth.mjs.map +1 -1
  21. package/dist/server/controllers/validation/user.js +16 -9
  22. package/dist/server/controllers/validation/user.js.map +1 -1
  23. package/dist/server/controllers/validation/user.mjs +16 -9
  24. package/dist/server/controllers/validation/user.mjs.map +1 -1
  25. package/dist/server/routes/content-api/auth.js +16 -0
  26. package/dist/server/routes/content-api/auth.js.map +1 -1
  27. package/dist/server/routes/content-api/auth.mjs +16 -0
  28. package/dist/server/routes/content-api/auth.mjs.map +1 -1
  29. package/dist/server/routes/content-api/validation.js +29 -2
  30. package/dist/server/routes/content-api/validation.js.map +1 -1
  31. package/dist/server/routes/content-api/validation.mjs +29 -2
  32. package/dist/server/routes/content-api/validation.mjs.map +1 -1
  33. package/dist/server/services/jwt.js +8 -6
  34. package/dist/server/services/jwt.js.map +1 -1
  35. package/dist/server/services/jwt.mjs +8 -6
  36. package/dist/server/services/jwt.mjs.map +1 -1
  37. package/dist/server/services/users-permissions.js +8 -0
  38. package/dist/server/services/users-permissions.js.map +1 -1
  39. package/dist/server/services/users-permissions.mjs +8 -0
  40. package/dist/server/services/users-permissions.mjs.map +1 -1
  41. package/dist/server/strategies/users-permissions.js +8 -1
  42. package/dist/server/strategies/users-permissions.js.map +1 -1
  43. package/dist/server/strategies/users-permissions.mjs +8 -1
  44. package/dist/server/strategies/users-permissions.mjs.map +1 -1
  45. package/dist/server/utils/refresh-cookie-options.js +27 -0
  46. package/dist/server/utils/refresh-cookie-options.js.map +1 -0
  47. package/dist/server/utils/refresh-cookie-options.mjs +25 -0
  48. package/dist/server/utils/refresh-cookie-options.mjs.map +1 -0
  49. package/lint-staged.config.mjs +1 -0
  50. package/package.json +5 -5
  51. package/rollup.config.mjs +3 -3
  52. package/server/controllers/auth.js +162 -141
  53. package/server/controllers/validation/user.js +22 -9
  54. package/server/routes/content-api/auth.js +12 -0
  55. package/server/routes/content-api/validation.js +32 -2
  56. package/server/services/jwt.js +4 -3
  57. package/server/services/users-permissions.js +2 -0
  58. package/server/strategies/users-permissions.js +6 -1
  59. package/server/utils/refresh-cookie-options.js +20 -0
@@ -6,6 +6,17 @@ const deleteRoleSchema = yup.object().shape({
6
6
  role: yup.strapiID().required(),
7
7
  });
8
8
 
9
+ // A role relation entry may be referenced by numeric `id` (legacy) or by
10
+ // `documentId` (the v5 default), but must carry at least one of them.
11
+ const roleRelationEntrySchema = yup
12
+ .object()
13
+ .shape({ id: yup.strapiID(), documentId: yup.strapiID() })
14
+ .test(
15
+ 'id-or-documentId',
16
+ 'Relation entry must include an id or documentId',
17
+ (entry) => !!entry && (entry.id != null || entry.documentId != null)
18
+ );
19
+
9
20
  const createUserBodySchema = yup.object().shape({
10
21
  email: yup.string().email().required(),
11
22
  username: yup.string().min(1).required(),
@@ -17,7 +28,7 @@ const createUserBodySchema = yup.object().shape({
17
28
  .shape({
18
29
  connect: yup
19
30
  .array()
20
- .of(yup.object().shape({ id: yup.strapiID().required() }))
31
+ .of(roleRelationEntrySchema)
21
32
  .min(1, 'Users must have a role')
22
33
  .required(),
23
34
  })
@@ -44,20 +55,22 @@ const updateUserBodySchema = yup.object().shape({
44
55
  role: yup.lazy((value) =>
45
56
  typeof value === 'object'
46
57
  ? yup.object().shape({
47
- connect: yup
48
- .array()
49
- .of(yup.object().shape({ id: yup.strapiID().required() }))
50
- .required(),
58
+ // connect/disconnect are each optional (matching core relation inputs),
59
+ // but a role must remain: reject disconnecting every role without
60
+ // connecting a replacement.
61
+ connect: yup.array().of(roleRelationEntrySchema),
51
62
  disconnect: yup
52
63
  .array()
53
- .test('CheckDisconnect', 'Cannot remove role', function test(disconnectValue) {
54
- if (value.connect.length === 0 && disconnectValue.length > 0) {
64
+ .of(roleRelationEntrySchema)
65
+ .test('CheckDisconnect', 'Cannot remove role', function test(disconnect) {
66
+ const connectValue = value.connect || [];
67
+ const disconnectValue = disconnect || [];
68
+ if (connectValue.length === 0 && disconnectValue.length > 0) {
55
69
  return false;
56
70
  }
57
71
 
58
72
  return true;
59
- })
60
- .required(),
73
+ }),
61
74
  })
62
75
  : yup.strapiID()
63
76
  ),
@@ -127,5 +127,17 @@ module.exports = (strapi) => {
127
127
  handler: 'auth.logout',
128
128
  config: { prefix: '' },
129
129
  },
130
+ {
131
+ method: 'GET',
132
+ path: '/auth/sessions',
133
+ handler: 'auth.getSessions',
134
+ config: { prefix: '' },
135
+ },
136
+ {
137
+ method: 'DELETE',
138
+ path: '/auth/sessions/:sessionId',
139
+ handler: 'auth.revokeSession',
140
+ config: { prefix: '' },
141
+ },
130
142
  ];
131
143
  };
@@ -3,6 +3,36 @@
3
3
  const { AbstractRouteValidator } = require('@strapi/utils');
4
4
  const z = require('zod/v4');
5
5
 
6
+ // A single role relation entry, referenced by numeric `id` (legacy) or by
7
+ // `documentId` (the v5 default), but must carry at least one of them. The
8
+ // detailed rules (min one role on create, cannot remove the last role on
9
+ // update) stay in the Yup controller validator.
10
+ const roleRelationEntry = z
11
+ .object({
12
+ id: z.union([z.number(), z.string()]).optional(),
13
+ documentId: z.string().optional(),
14
+ })
15
+ .refine((entry) => entry.id != null || entry.documentId != null, {
16
+ message: 'Relation entry must include an id or documentId',
17
+ });
18
+
19
+ // The `role` relation input: shorthand scalar (numeric id or documentId) or the
20
+ // longhand connect/disconnect object, matching every other v5 relation. The
21
+ // object form must carry at least one of connect/disconnect (an empty object is
22
+ // a no-op and almost certainly a mistake).
23
+ const roleRelationInput = z.union([
24
+ z.number(),
25
+ z.string(),
26
+ z
27
+ .object({
28
+ connect: z.array(roleRelationEntry).optional(),
29
+ disconnect: z.array(roleRelationEntry).optional(),
30
+ })
31
+ .refine((input) => input.connect != null || input.disconnect != null, {
32
+ message: 'Relation input must include connect or disconnect',
33
+ }),
34
+ ]);
35
+
6
36
  class UsersPermissionsRouteValidator extends AbstractRouteValidator {
7
37
  constructor(strapi) {
8
38
  super();
@@ -201,7 +231,7 @@ class UsersPermissionsRouteValidator extends AbstractRouteValidator {
201
231
  username: z.string(),
202
232
  email: z.email(),
203
233
  password: z.string(),
204
- role: z.number().optional(),
234
+ role: roleRelationInput.optional(),
205
235
  });
206
236
  }
207
237
 
@@ -210,7 +240,7 @@ class UsersPermissionsRouteValidator extends AbstractRouteValidator {
210
240
  username: z.string().optional(),
211
241
  email: z.email().optional(),
212
242
  password: z.string().optional(),
213
- role: z.number().optional(),
243
+ role: roleRelationInput.optional(),
214
244
  });
215
245
  }
216
246
 
@@ -80,17 +80,18 @@ module.exports = ({ strapi }) => ({
80
80
  throw new Error('Invalid token.');
81
81
  }
82
82
 
83
- return { id: user.id };
83
+ // Surface the sessionId so the strategy can flag the "current" session.
84
+ return { id: user.id, sessionId: result.payload.sessionId };
84
85
  }
85
86
 
86
87
  return new Promise((resolve, reject) => {
87
88
  const jwtConfig = strapi.config.get('plugin::users-permissions.jwt', {});
88
- const algorithms = jwtConfig && jwtConfig.algorithm ? [jwtConfig.algorithm] : undefined;
89
+ const algorithms = [jwtConfig?.algorithm || 'HS256'];
89
90
 
90
91
  jwt.verify(
91
92
  token,
92
93
  strapi.config.get('plugin::users-permissions.jwtSecret'),
93
- algorithms ? { algorithms } : {},
94
+ { algorithms },
94
95
  (err, tokenPayload = {}) => {
95
96
  if (err) {
96
97
  return reject(new Error('Invalid token.'));
@@ -22,6 +22,8 @@ const DEFAULT_PERMISSIONS = [
22
22
  { action: 'plugin::users-permissions.auth.sendEmailConfirmation', roleType: 'public' },
23
23
  { action: 'plugin::users-permissions.auth.refresh', roleType: 'public' },
24
24
  { action: 'plugin::users-permissions.auth.logout', roleType: 'authenticated' },
25
+ { action: 'plugin::users-permissions.auth.getSessions', roleType: 'authenticated' },
26
+ { action: 'plugin::users-permissions.auth.revokeSession', roleType: 'authenticated' },
25
27
  { action: 'plugin::users-permissions.user.me', roleType: 'authenticated' },
26
28
  { action: 'plugin::users-permissions.auth.changePassword', roleType: 'authenticated' },
27
29
  ];
@@ -14,7 +14,7 @@ const authenticate = async (ctx) => {
14
14
  const token = await getService('jwt').getToken(ctx);
15
15
 
16
16
  if (token) {
17
- const { id } = token;
17
+ const { id, sessionId } = token;
18
18
 
19
19
  // Invalid token
20
20
  if (id === undefined) {
@@ -49,6 +49,11 @@ const authenticate = async (ctx) => {
49
49
  const ability = await strapi.contentAPI.permissions.engine.generateAbility(permissions);
50
50
 
51
51
  ctx.state.user = user;
52
+ // Expose the session backing this request (refresh mode only) so endpoints
53
+ // can flag the "current" session when listing active sessions.
54
+ if (sessionId) {
55
+ ctx.state.session = { id: sessionId };
56
+ }
52
57
 
53
58
  return {
54
59
  authenticated: true,
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const buildRefreshCookieOptions = (upSessions, isProduction) => {
4
+ const isSecure =
5
+ typeof upSessions.cookie?.secure === 'boolean' ? upSessions.cookie?.secure : isProduction;
6
+
7
+ return {
8
+ httpOnly: true,
9
+ secure: isSecure,
10
+ sameSite: upSessions.cookie?.sameSite ?? 'lax',
11
+ path: upSessions.cookie?.path ?? '/',
12
+ domain: upSessions.cookie?.domain,
13
+ maxAge: upSessions.cookie?.maxAge,
14
+ overwrite: true,
15
+ };
16
+ };
17
+
18
+ module.exports = {
19
+ buildRefreshCookieOptions,
20
+ };