@yunsoft/yuncms-core 0.1.3 → 0.1.6

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.
@@ -63,6 +63,13 @@ async function assertRoleAssignable(database, role, accountability) {
63
63
  if (targetRole.admin && accountability.admin !== true && accountability.system !== true) {
64
64
  throw forbidden('Only an administrator can assign the administrator role');
65
65
  }
66
+ if (
67
+ accountability.admin !== true
68
+ && accountability.system !== true
69
+ && role !== accountability.role
70
+ ) {
71
+ throw forbidden('Delegated user managers may assign only their own role');
72
+ }
66
73
  }
67
74
 
68
75
  async function assertTargetManageable(database, id, accountability) {
@@ -84,6 +91,15 @@ async function assertTargetManageable(database, id, accountability) {
84
91
  }
85
92
 
86
93
  export class UsersService extends BaseService {
94
+ async action(event, payload) {
95
+ if (!this.emitter) return;
96
+ await this.emitter.action(event, payload, {
97
+ accountability: this.accountability,
98
+ requestId: this.requestId,
99
+ collection: 'yuncms_users',
100
+ });
101
+ }
102
+
87
103
  async #readOneUnsafe(id) {
88
104
  const [rows] = await this.database.query(
89
105
  `SELECT id, email, role, status, email_verified_at, last_access, created_at, updated_at
@@ -133,7 +149,9 @@ export class UsersService extends BaseService {
133
149
  ],
134
150
  );
135
151
 
136
- return this.#readOneUnsafe(id);
152
+ const user = await this.#readOneUnsafe(id);
153
+ await this.action('users.create', { key: id, item: user });
154
+ return user;
137
155
  }
138
156
 
139
157
  async updateOne(id, patch = {}) {
@@ -163,7 +181,7 @@ export class UsersService extends BaseService {
163
181
  await assertRoleAssignable(this.database, patch.role, this.accountability);
164
182
  }
165
183
 
166
- return withTransaction(this.database, async (connection) => {
184
+ const user = await withTransaction(this.database, async (connection) => {
167
185
  const assignments = [];
168
186
  const params = [];
169
187
 
@@ -202,6 +220,8 @@ export class UsersService extends BaseService {
202
220
  );
203
221
  return rows[0] ?? null;
204
222
  });
223
+ await this.action('users.update', { key: id, item: user, changes: patch });
224
+ return user;
205
225
  }
206
226
 
207
227
  async deleteOne(id) {
@@ -212,12 +232,14 @@ export class UsersService extends BaseService {
212
232
  error.code = 'SELF_ADMIN_MUTATION_FORBIDDEN';
213
233
  throw error;
214
234
  }
235
+ const before = this.emitter ? await this.#readOneUnsafe(id) : null;
215
236
  const [result] = await this.database.query('DELETE FROM yuncms_users WHERE id = ?', [id]);
216
237
  if (result.affectedRows !== 1) {
217
238
  const error = new Error(`Unknown user: ${id}`);
218
239
  error.code = 'USER_NOT_FOUND';
219
240
  throw error;
220
241
  }
242
+ await this.action('users.delete', { key: id, before });
221
243
  return true;
222
244
  }
223
245
 
@@ -241,7 +263,6 @@ export class UsersService extends BaseService {
241
263
  }
242
264
  await connection.query('DELETE FROM yuncms_sessions WHERE user = ?', [id]);
243
265
  await connection.commit();
244
- return true;
245
266
  } catch (error) {
246
267
  try {
247
268
  await connection.rollback();
@@ -252,6 +273,9 @@ export class UsersService extends BaseService {
252
273
  } finally {
253
274
  connection.release();
254
275
  }
276
+
277
+ await this.action('users.password.update', { key: id });
278
+ return true;
255
279
  }
256
280
  }
257
281
 
@@ -1,4 +1,5 @@
1
1
  const ALL_ACTIONS = Object.freeze(['read', 'create', 'update', 'delete']);
2
+ const PERMISSION_MODES = new Set(['action-only', 'filter-read']);
2
3
 
3
4
  function parseMetadata(value) {
4
5
  if (value == null) return {};
@@ -14,16 +15,28 @@ function isEnabledFlag(value) {
14
15
  return value === true || value === 1;
15
16
  }
16
17
 
18
+ function advancedUnsupported(collectionSchema, message) {
19
+ const error = new Error(message ?? `System resource ${collectionSchema.collection} does not support this advanced permission rule`);
20
+ error.code = 'SYSTEM_PERMISSION_ADVANCED_UNSUPPORTED';
21
+ throw error;
22
+ }
23
+
17
24
  export function systemPermissionConfig(collectionSchema) {
18
25
  if (!collectionSchema?.system) return null;
19
26
  const metadata = parseMetadata(collectionSchema.metadata);
20
27
  if (!isEnabledFlag(metadata.permissionManaged)) return null;
28
+ const mode = metadata.permissionMode ?? 'action-only';
29
+ if (!PERMISSION_MODES.has(mode)) {
30
+ const error = new Error(`Unsupported system permission mode for ${collectionSchema.collection}: ${mode}`);
31
+ error.code = 'SYSTEM_PERMISSION_MODE_UNSUPPORTED';
32
+ throw error;
33
+ }
21
34
  const allowedActions = Array.isArray(metadata.allowedActions)
22
35
  ? metadata.allowedActions.filter((action) => ALL_ACTIONS.includes(action))
23
36
  : [];
24
37
  return Object.freeze({
25
38
  resource: metadata.resource ?? collectionSchema.collection,
26
- mode: metadata.permissionMode ?? 'action-only',
39
+ mode,
27
40
  allowedActions: Object.freeze([...new Set(allowedActions)]),
28
41
  });
29
42
  }
@@ -43,12 +56,42 @@ export function assertSystemResourceAction(collectionSchema, action) {
43
56
  return config;
44
57
  }
45
58
 
46
- export function assertActionOnlyPermissionPayload(collectionSchema, { fields, filter, validation } = {}) {
59
+ export function assertSystemPermissionPayload(
60
+ collectionSchema,
61
+ action,
62
+ { fields, filter, validation } = {},
63
+ ) {
47
64
  const config = systemPermissionConfig(collectionSchema);
48
- if (!config || config.mode !== 'action-only') return;
49
- if (fields != null || filter != null || validation != null) {
50
- const error = new Error(`System resource ${collectionSchema.collection} supports action-level permissions only`);
51
- error.code = 'SYSTEM_PERMISSION_ADVANCED_UNSUPPORTED';
52
- throw error;
65
+ if (!config) return;
66
+
67
+ if (config.mode === 'action-only') {
68
+ if (fields != null || filter != null || validation != null) {
69
+ advancedUnsupported(
70
+ collectionSchema,
71
+ `System resource ${collectionSchema.collection} supports action-level permissions only`,
72
+ );
73
+ }
74
+ return;
53
75
  }
76
+
77
+ if (config.mode === 'filter-read') {
78
+ if (fields != null || validation != null) {
79
+ advancedUnsupported(
80
+ collectionSchema,
81
+ `System resource ${collectionSchema.collection} supports only a row filter on read permissions`,
82
+ );
83
+ }
84
+ if (filter != null && action !== 'read') {
85
+ advancedUnsupported(
86
+ collectionSchema,
87
+ `System resource ${collectionSchema.collection} permits row filters only for read`,
88
+ );
89
+ }
90
+ }
91
+ }
92
+
93
+ export function assertActionOnlyPermissionPayload(collectionSchema, payload = {}) {
94
+ const config = systemPermissionConfig(collectionSchema);
95
+ if (!config || config.mode !== 'action-only') return;
96
+ return assertSystemPermissionPayload(collectionSchema, 'read', payload);
54
97
  }