@managemint-solutions/sdk 0.52.0 → 0.54.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 (72) hide show
  1. package/README.md +92 -13
  2. package/dist/additional-details/index.d.ts +2 -2
  3. package/dist/additional-details/index.js +6 -10
  4. package/dist/audit/dto.d.ts +3 -8
  5. package/dist/audit/dto.js +7 -48
  6. package/dist/audit/index.d.ts +71 -10
  7. package/dist/audit/index.js +234 -55
  8. package/dist/auth-client/index.d.ts +2 -0
  9. package/dist/auth-client/index.js +12 -0
  10. package/dist/auth.d.ts +21 -0
  11. package/dist/auth.js +47 -3
  12. package/dist/clients/dto.d.ts +5 -1
  13. package/dist/clients/dto.js +12 -1
  14. package/dist/clients/index.d.ts +8 -4
  15. package/dist/clients/index.js +11 -8
  16. package/dist/emulation-handshakes/index.d.ts +13 -0
  17. package/dist/emulation-handshakes/index.js +36 -0
  18. package/dist/errors.js +2 -2
  19. package/dist/index.d.ts +14 -10
  20. package/dist/index.js +23 -4
  21. package/dist/leads/dto.d.ts +5 -1
  22. package/dist/leads/dto.js +12 -1
  23. package/dist/leads/index.d.ts +8 -5
  24. package/dist/leads/index.js +23 -21
  25. package/dist/leave/leave-adjustments.js +1 -0
  26. package/dist/leave/leave-entitlements.d.ts +3 -1
  27. package/dist/leave/leave-entitlements.js +3 -0
  28. package/dist/leave/leave-requests.d.ts +3 -1
  29. package/dist/leave/leave-requests.js +3 -0
  30. package/dist/nest/index.d.ts +26 -1
  31. package/dist/nest/index.js +58 -1
  32. package/dist/notifications/index.d.ts +1 -3
  33. package/dist/organization-payment-methods/index.d.ts +5 -6
  34. package/dist/organization-payment-methods/index.js +4 -17
  35. package/dist/organizations/dto.d.ts +9 -1
  36. package/dist/organizations/dto.js +40 -1
  37. package/dist/organizations/index.d.ts +13 -3
  38. package/dist/organizations/index.js +59 -3
  39. package/dist/permissions/index.d.ts +6 -4
  40. package/dist/permissions/index.js +19 -12
  41. package/dist/projects/dto.d.ts +5 -1
  42. package/dist/projects/dto.js +12 -1
  43. package/dist/projects/index.d.ts +8 -4
  44. package/dist/projects/index.js +11 -8
  45. package/dist/service-client/index.d.ts +39 -1
  46. package/dist/service-client/index.js +89 -2
  47. package/dist/staff/dto.d.ts +14 -0
  48. package/dist/staff/dto.js +63 -0
  49. package/dist/staff/index.d.ts +50 -0
  50. package/dist/staff/index.js +139 -0
  51. package/dist/staff-client.d.ts +37 -0
  52. package/dist/staff-client.js +52 -0
  53. package/dist/tasks/dto.d.ts +5 -1
  54. package/dist/tasks/dto.js +12 -1
  55. package/dist/tasks/index.d.ts +15 -10
  56. package/dist/tasks/index.js +34 -31
  57. package/dist/tenancy.d.ts +13 -0
  58. package/dist/tenancy.js +25 -0
  59. package/dist/timesheets/timesheet-entries.js +1 -0
  60. package/dist/timesheets/timesheets.d.ts +3 -1
  61. package/dist/timesheets/timesheets.js +3 -0
  62. package/dist/user-api-credentials/dto.d.ts +12 -0
  63. package/dist/user-api-credentials/dto.js +56 -0
  64. package/dist/user-api-credentials/index.d.ts +47 -0
  65. package/dist/user-api-credentials/index.js +101 -0
  66. package/dist/users/dto.d.ts +5 -1
  67. package/dist/users/dto.js +13 -2
  68. package/dist/users/index.d.ts +7 -3
  69. package/dist/users/index.js +16 -29
  70. package/dist/validators.d.ts +2 -0
  71. package/dist/validators.js +6 -1
  72. package/package.json +2 -2
@@ -1,17 +1,21 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { TaskEntity, TaskSummary } from '@managemint-solutions/entities/tasks';
3
- import type { AuthContext } from '../auth';
4
- import { type AuditResource } from '../audit';
3
+ import type { ClientContext } from '../auth';
4
+ import { EntityAuditTrail, type AuditResource } from '../audit';
5
5
  import type { TypedSupabaseClient } from '../client';
6
6
  import type { AddTaskLinkDto, CreateTaskDto, GetTasksDto, UpdateTaskDto } from './dto';
7
7
  export * from './dto';
8
8
  export declare class TasksResource {
9
9
  private readonly supabase;
10
10
  private readonly auth;
11
- private readonly audit;
11
+ private readonly auditLog;
12
12
  private readonly table;
13
- constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
14
- list(query: GetTasksDto): Promise<MmsList<TaskSummary>>;
13
+ /** This record's history - its own rows and the rows that hang off it. */
14
+ readonly audit: EntityAuditTrail;
15
+ constructor(supabase: TypedSupabaseClient, auth: ClientContext, auditLog: AuditResource);
16
+ list(query: GetTasksDto & {
17
+ organization_id?: string | null;
18
+ }): Promise<MmsList<TaskSummary>>;
15
19
  getById(taskId: string): Promise<TaskEntity>;
16
20
  create(input: CreateTaskDto): Promise<TaskEntity>;
17
21
  update(taskId: string, input: UpdateTaskDto): Promise<TaskEntity>;
@@ -43,11 +47,12 @@ export declare class TasksResource {
43
47
  private checkLabels;
44
48
  /**
45
49
  * Makes `labelIds` the task's whole label set. The join rows carry no audit of their own: the
46
- * change is one UPDATE entry on the task, `labels: { old: [names], new: [names] }`, recorded
47
- * only when the set actually moved - on a new task that entry is the record of the initial
48
- * pinning. Only the difference is written, the new labels inserted before the dropped ones are
49
- * deleted, so should the trigger still refuse one (a label deleted after `checkLabels`) the
50
- * task's existing labels are untouched, and a label kept across an edit keeps its place.
50
+ * change is an UPDATE entry on the task, `labels: { old: [names], new: [names] }`, recorded
51
+ * only when the set actually moved - inside a request's audit batch it folds into the task's
52
+ * own entry for that save (`withAuditBatch`). Only the difference is written, the new labels
53
+ * inserted before the dropped ones are deleted, so should the trigger still refuse one (a label
54
+ * deleted after `checkLabels`) the task's existing labels are untouched, and a label kept
55
+ * across an edit keeps its place.
51
56
  */
52
57
  private setLabels;
53
58
  }
@@ -20,6 +20,7 @@ const enum_2 = require("@managemint-solutions/entities/labels/enum");
20
20
  const enum_3 = require("@managemint-solutions/entities/audit-trail/enum");
21
21
  const audit_1 = require("../audit");
22
22
  const errors_1 = require("../errors");
23
+ const tenancy_1 = require("../tenancy");
23
24
  const transforms_1 = require("../transforms");
24
25
  __exportStar(require("./dto"), exports);
25
26
  // The public contract is the shared TaskSummary / TaskEntity from @managemint-solutions/entities;
@@ -124,16 +125,19 @@ const linkedTasks = (rows, linkType) => rows
124
125
  class TasksResource {
125
126
  supabase;
126
127
  auth;
127
- audit;
128
+ auditLog;
128
129
  table;
129
- constructor(supabase, auth, audit) {
130
+ /** This record's history - its own rows and the rows that hang off it. */
131
+ audit;
132
+ constructor(supabase, auth, auditLog) {
130
133
  this.supabase = supabase;
131
134
  this.auth = auth;
132
- this.audit = audit;
133
- this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
135
+ this.auditLog = auditLog;
136
+ this.table = (0, audit_1.auditedTable)(supabase, auth, auditLog, {
134
137
  table: 'tasks',
135
138
  entityType: enum_3.AuditTrailEntityType.TASKS,
136
139
  });
140
+ this.audit = new audit_1.EntityAuditTrail(supabase, auth, enum_3.AuditTrailEntityType.TASKS);
137
141
  }
138
142
  async list(query) {
139
143
  const page = query.page;
@@ -143,17 +147,17 @@ class TasksResource {
143
147
  const labelIdIn = query.label_id_in && query.label_id_in.length > 0 ? query.label_id_in : null;
144
148
  // `order`/`range` return `this`, so the builder stays a filter builder and the conditional
145
149
  // filters below can be reassigned onto it without a cast.
146
- let tasksQuery = this.supabase
150
+ let tasksQuery = (0, tenancy_1.scopeToOrganization)(this.supabase
147
151
  .from('tasks')
148
- .select(labelIdIn ? `${TASK_LIST_SELECT}, ${LABEL_FILTER_SELECT}` : TASK_LIST_SELECT, {
149
- count: 'estimated',
150
- })
151
- .eq('organization_id', this.auth.organization_id)
152
+ .select((0, tenancy_1.withOrganization)(labelIdIn ? `${TASK_LIST_SELECT}, ${LABEL_FILTER_SELECT}` : TASK_LIST_SELECT, this.auth), { count: 'estimated' }), this.auth)
152
153
  .eq('archived', query.archive)
153
154
  .eq('deleted', false)
154
155
  .order('priority', { ascending: false })
155
156
  .order('created_at', { ascending: false })
156
157
  .range(offset, offset + pageSize - 1);
158
+ if (query.organization_id) {
159
+ tasksQuery = tasksQuery.eq('organization_id', query.organization_id);
160
+ }
157
161
  if (trimmedSearch) {
158
162
  // "TASK-12", "#12" or "12" also finds task number 12; anything else is a text search only.
159
163
  const taskNumber = trimmedSearch.match(/^(?:[a-z]+-|#)?(\d+)$/i)?.[1];
@@ -217,11 +221,7 @@ class TasksResource {
217
221
  return (0, transforms_1.toMmsList)(rows, page, pageSize, count);
218
222
  }
219
223
  async getById(taskId) {
220
- const { data, error } = await this.supabase
221
- .from('tasks')
222
- .select(TASK_DETAIL_SELECT)
223
- .eq('mms_id', taskId)
224
- .eq('organization_id', this.auth.organization_id)
224
+ const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('tasks').select(TASK_DETAIL_SELECT).eq('mms_id', taskId), this.auth)
225
225
  .order('created_at', { referencedTable: 'links_out' })
226
226
  .order('created_at', { referencedTable: 'links_in' })
227
227
  .single();
@@ -247,7 +247,7 @@ class TasksResource {
247
247
  // BEFORE INSERT trigger fills it in when it arrives null.
248
248
  const row = await this.table.insert({
249
249
  ...fields,
250
- organization_id: this.auth.organization_id,
250
+ organization_id: (0, tenancy_1.requireOrganization)(this.auth),
251
251
  created_by: this.auth.mms_id,
252
252
  created_at: new Date().toISOString(),
253
253
  });
@@ -297,6 +297,7 @@ class TasksResource {
297
297
  * made from.
298
298
  */
299
299
  async linkTask(taskId, input) {
300
+ const organizationId = (0, tenancy_1.requireOrganization)(this.auth);
300
301
  const refuse = (status, message, cause) => new errors_1.SupabaseClientError({ status, error: 'Database Error', message }, { cause });
301
302
  if (input.linked_task_id === taskId) {
302
303
  throw refuse(400, 'A task cannot be linked to itself');
@@ -304,7 +305,7 @@ class TasksResource {
304
305
  const { data: tasks, error: tasksError } = await this.supabase
305
306
  .from('tasks')
306
307
  .select('mms_id, task_number:task_id')
307
- .eq('organization_id', this.auth.organization_id)
308
+ .eq('organization_id', organizationId)
308
309
  .eq('deleted', false)
309
310
  .in('mms_id', [taskId, input.linked_task_id]);
310
311
  if (tasksError)
@@ -325,7 +326,7 @@ class TasksResource {
325
326
  const { data: parentLink, error: parentError } = await this.supabase
326
327
  .from('task_links')
327
328
  .select('task_id')
328
- .eq('organization_id', this.auth.organization_id)
329
+ .eq('organization_id', organizationId)
329
330
  .eq('linked_task_id', child)
330
331
  .eq('link_type', enum_1.TaskLinkType.CHILD)
331
332
  .maybeSingle();
@@ -341,7 +342,7 @@ class TasksResource {
341
342
  task_id: taskId,
342
343
  linked_task_id: input.linked_task_id,
343
344
  link_type: input.link_type,
344
- organization_id: this.auth.organization_id,
345
+ organization_id: organizationId,
345
346
  created_by: this.auth.mms_id,
346
347
  });
347
348
  if (error) {
@@ -356,7 +357,7 @@ class TasksResource {
356
357
  throw (0, errors_1.mapPostgrestError)(error);
357
358
  }
358
359
  const role = input.link_type === enum_1.TaskLinkType.CHILD ? 'subtask' : 'related';
359
- await this.audit.record({
360
+ await this.auditLog.record({
360
361
  action: enum_3.AuditTrailAction.UPDATE,
361
362
  entityType: enum_3.AuditTrailEntityType.TASKS,
362
363
  entityId: taskId,
@@ -373,7 +374,7 @@ class TasksResource {
373
374
  const { data, error } = await this.supabase
374
375
  .from('task_links')
375
376
  .delete()
376
- .eq('organization_id', this.auth.organization_id)
377
+ .eq('organization_id', (0, tenancy_1.requireOrganization)(this.auth))
377
378
  .or(`and(task_id.eq.${taskId},linked_task_id.eq.${linkedTaskId}),and(task_id.eq.${linkedTaskId},linked_task_id.eq.${taskId})`)
378
379
  .select('task_id, link_type, from:tasks!task_id ( task_number:task_id ), to:tasks!linked_task_id ( task_number:task_id )');
379
380
  if (error)
@@ -384,7 +385,7 @@ class TasksResource {
384
385
  }
385
386
  const madeHere = row.task_id === taskId;
386
387
  const role = row.link_type === enum_1.TaskLinkType.RELATED ? 'related' : madeHere ? 'subtask' : 'parent';
387
- await this.audit.record({
388
+ await this.auditLog.record({
388
389
  action: enum_3.AuditTrailAction.UPDATE,
389
390
  entityType: enum_3.AuditTrailEntityType.TASKS,
390
391
  entityId: taskId,
@@ -406,7 +407,7 @@ class TasksResource {
406
407
  const { count, error } = await this.supabase
407
408
  .from('labels')
408
409
  .select('mms_id', { count: 'exact', head: true })
409
- .eq('organization_id', this.auth.organization_id)
410
+ .eq('organization_id', (0, tenancy_1.requireOrganization)(this.auth))
410
411
  .eq('entity_type', enum_2.LabelEntityTypes.TASK)
411
412
  .in('mms_id', labelIds);
412
413
  if (error)
@@ -421,18 +422,20 @@ class TasksResource {
421
422
  }
422
423
  /**
423
424
  * Makes `labelIds` the task's whole label set. The join rows carry no audit of their own: the
424
- * change is one UPDATE entry on the task, `labels: { old: [names], new: [names] }`, recorded
425
- * only when the set actually moved - on a new task that entry is the record of the initial
426
- * pinning. Only the difference is written, the new labels inserted before the dropped ones are
427
- * deleted, so should the trigger still refuse one (a label deleted after `checkLabels`) the
428
- * task's existing labels are untouched, and a label kept across an edit keeps its place.
425
+ * change is an UPDATE entry on the task, `labels: { old: [names], new: [names] }`, recorded
426
+ * only when the set actually moved - inside a request's audit batch it folds into the task's
427
+ * own entry for that save (`withAuditBatch`). Only the difference is written, the new labels
428
+ * inserted before the dropped ones are deleted, so should the trigger still refuse one (a label
429
+ * deleted after `checkLabels`) the task's existing labels are untouched, and a label kept
430
+ * across an edit keeps its place.
429
431
  */
430
432
  async setLabels(taskId, labelIds) {
433
+ const organizationId = (0, tenancy_1.requireOrganization)(this.auth);
431
434
  const { data: current, error: readError } = await this.supabase
432
435
  .from('task_labels')
433
436
  .select(PINNED_LABEL_SELECT)
434
437
  .eq('task_id', taskId)
435
- .eq('organization_id', this.auth.organization_id)
438
+ .eq('organization_id', organizationId)
436
439
  .order('created_at');
437
440
  if (readError)
438
441
  throw (0, errors_1.mapPostgrestError)(readError);
@@ -449,7 +452,7 @@ class TasksResource {
449
452
  .insert(added.map((labelId) => ({
450
453
  task_id: taskId,
451
454
  label_id: labelId,
452
- organization_id: this.auth.organization_id,
455
+ organization_id: organizationId,
453
456
  created_by: this.auth.mms_id,
454
457
  })))
455
458
  .select(PINNED_LABEL_SELECT);
@@ -462,12 +465,12 @@ class TasksResource {
462
465
  .from('task_labels')
463
466
  .delete()
464
467
  .eq('task_id', taskId)
465
- .eq('organization_id', this.auth.organization_id)
468
+ .eq('organization_id', organizationId)
466
469
  .in('label_id', dropped.map((row) => row.label_id));
467
470
  if (error)
468
471
  throw (0, errors_1.mapPostgrestError)(error);
469
472
  }
470
- await this.audit.record({
473
+ await this.auditLog.record({
471
474
  action: enum_3.AuditTrailAction.UPDATE,
472
475
  entityType: enum_3.AuditTrailEntityType.TASKS,
473
476
  entityId: taskId,
@@ -0,0 +1,13 @@
1
+ import type { PostgrestFilterBuilder } from '@supabase/postgrest-js';
2
+ /** A read filtered to the caller's organization, or left across every organization for staff. */
3
+ export declare const scopeToOrganization: <Q extends PostgrestFilterBuilder<any, any, any, any, any, any, any>>(query: Q, actor: {
4
+ organization_id: string | null;
5
+ }) => Q;
6
+ /** The caller's organization id, for a write; staff have none and are refused with a 403. */
7
+ export declare const requireOrganization: (actor: {
8
+ organization_id: string | null;
9
+ }) => string;
10
+ /** A list select, plus each row's organization on a staff read so the row says whose it is. */
11
+ export declare const withOrganization: (select: string, actor: {
12
+ organization_id: string | null;
13
+ }) => string;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withOrganization = exports.requireOrganization = exports.scopeToOrganization = void 0;
4
+ const errors_1 = require("./errors");
5
+ // How a resource scopes itself to the caller's organization. A tenant (`AuthContext`) reads and
6
+ // writes inside their own; staff (`StaffContext`, `organization_id: null`) read across every
7
+ // organization, as their role's policies allow, and write nothing.
8
+ /** A read filtered to the caller's organization, or left across every organization for staff. */
9
+ const scopeToOrganization = (query, actor) => (actor.organization_id === null ? query : query.eq('organization_id', actor.organization_id));
10
+ exports.scopeToOrganization = scopeToOrganization;
11
+ /** The caller's organization id, for a write; staff have none and are refused with a 403. */
12
+ const requireOrganization = (actor) => {
13
+ if (actor.organization_id === null) {
14
+ throw new errors_1.SupabaseClientError({
15
+ status: 403,
16
+ error: 'Permission Error',
17
+ message: 'Organization required',
18
+ });
19
+ }
20
+ return actor.organization_id;
21
+ };
22
+ exports.requireOrganization = requireOrganization;
23
+ /** A list select, plus each row's organization on a staff read so the row says whose it is. */
24
+ const withOrganization = (select, actor) => actor.organization_id === null ? `${select}, organization:organizations!organization_id(mms_id, name)` : select;
25
+ exports.withOrganization = withOrganization;
@@ -59,6 +59,7 @@ class TimesheetEntriesResource {
59
59
  this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
60
60
  table: 'timesheet_entries',
61
61
  entityType: enum_1.AuditTrailEntityType.TIMESHEET_ENTRIES,
62
+ related: { entityType: enum_1.AuditTrailEntityType.TIMESHEETS, column: 'timesheet_id' },
62
63
  });
63
64
  }
64
65
  async getById(timesheetEntryId) {
@@ -2,7 +2,7 @@ import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { TimesheetEntity, TimesheetSummary } from '@managemint-solutions/entities/timesheets';
3
3
  import { TimesheetReviewDecision, TimesheetStatus } from '@managemint-solutions/entities/timesheets/enum';
4
4
  import type { AuthContext } from '../auth';
5
- import { type AuditResource } from '../audit';
5
+ import { EntityAuditTrail, type AuditResource } from '../audit';
6
6
  import type { TypedSupabaseClient } from '../client';
7
7
  import type { GetTimesheetsDto } from './dto';
8
8
  /** What the entry path needs of the week an entry falls in: its id and whether it is still open. */
@@ -29,6 +29,8 @@ export declare class TimesheetsResource {
29
29
  private readonly supabase;
30
30
  private readonly auth;
31
31
  private readonly table;
32
+ /** This record's history - its own rows and the rows that hang off it. */
33
+ readonly audit: EntityAuditTrail;
32
34
  constructor(supabase: TypedSupabaseClient, auth: AuthContext, audit: AuditResource);
33
35
  list(query: GetTimesheetsDto, options?: ListOptions): Promise<MmsList<TimesheetSummary>>;
34
36
  /** The week with its entries, by date then start time. */
@@ -27,6 +27,8 @@ class TimesheetsResource {
27
27
  supabase;
28
28
  auth;
29
29
  table;
30
+ /** This record's history - its own rows and the rows that hang off it. */
31
+ audit;
30
32
  constructor(supabase, auth, audit) {
31
33
  this.supabase = supabase;
32
34
  this.auth = auth;
@@ -34,6 +36,7 @@ class TimesheetsResource {
34
36
  table: 'timesheets',
35
37
  entityType: enum_1.AuditTrailEntityType.TIMESHEETS,
36
38
  });
39
+ this.audit = new audit_1.EntityAuditTrail(supabase, auth, enum_1.AuditTrailEntityType.TIMESHEETS);
37
40
  }
38
41
  async list(query, options = {}) {
39
42
  const page = query.page;
@@ -0,0 +1,12 @@
1
+ import type { CreateUserApiCredentialDto as CreateUserApiCredentialDtoType, MintEmulationTokenDto as MintEmulationTokenDtoType, UserApiCredentialIdDto as UserApiCredentialIdDtoType } from '@managemint-solutions/entities/user-api-credentials/dto';
2
+ export declare class UserApiCredentialIdDto implements UserApiCredentialIdDtoType {
3
+ readonly credentialId: string;
4
+ }
5
+ export declare class CreateUserApiCredentialDto implements CreateUserApiCredentialDtoType {
6
+ readonly label: string;
7
+ readonly expires_at?: string | null;
8
+ }
9
+ export declare class MintEmulationTokenDto implements MintEmulationTokenDtoType {
10
+ readonly client_id: string;
11
+ readonly client_secret: string;
12
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.MintEmulationTokenDto = exports.CreateUserApiCredentialDto = exports.UserApiCredentialIdDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ const validators_1 = require("../validators");
15
+ class UserApiCredentialIdDto {
16
+ credentialId;
17
+ }
18
+ exports.UserApiCredentialIdDto = UserApiCredentialIdDto;
19
+ __decorate([
20
+ (0, class_validator_1.IsUUID)('7', { message: 'Invalid credential ID format' }),
21
+ __metadata("design:type", String)
22
+ ], UserApiCredentialIdDto.prototype, "credentialId", void 0);
23
+ class CreateUserApiCredentialDto {
24
+ label;
25
+ expires_at;
26
+ }
27
+ exports.CreateUserApiCredentialDto = CreateUserApiCredentialDto;
28
+ __decorate([
29
+ (0, class_validator_1.IsString)({ message: 'Label is required' }),
30
+ (0, class_validator_1.MinLength)(1, { message: 'Label is required' }),
31
+ (0, class_validator_1.MaxLength)(100, { message: 'Label cannot be longer than 100 characters' }),
32
+ __metadata("design:type", String)
33
+ ], CreateUserApiCredentialDto.prototype, "label", void 0);
34
+ __decorate([
35
+ (0, class_validator_1.IsOptional)(),
36
+ (0, validators_1.IsNullable)(),
37
+ (0, class_validator_1.IsDateString)({}, { message: 'Expiry must be a valid ISO 8601 date string' }),
38
+ __metadata("design:type", Object)
39
+ ], CreateUserApiCredentialDto.prototype, "expires_at", void 0);
40
+ class MintEmulationTokenDto {
41
+ client_id;
42
+ client_secret;
43
+ }
44
+ exports.MintEmulationTokenDto = MintEmulationTokenDto;
45
+ __decorate([
46
+ (0, class_validator_1.IsString)({ message: 'Client ID is required' }),
47
+ (0, class_validator_1.MinLength)(1, { message: 'Client ID is required' }),
48
+ (0, class_validator_1.MaxLength)(255, { message: 'Client ID cannot be longer than 255 characters' }),
49
+ __metadata("design:type", String)
50
+ ], MintEmulationTokenDto.prototype, "client_id", void 0);
51
+ __decorate([
52
+ (0, class_validator_1.IsString)({ message: 'Client secret is required' }),
53
+ (0, class_validator_1.MinLength)(1, { message: 'Client secret is required' }),
54
+ (0, class_validator_1.MaxLength)(255, { message: 'Client secret cannot be longer than 255 characters' }),
55
+ __metadata("design:type", String)
56
+ ], MintEmulationTokenDto.prototype, "client_secret", void 0);
@@ -0,0 +1,47 @@
1
+ import type { UserApiCredentialEntity } from '@managemint-solutions/entities/user-api-credentials';
2
+ import { type OrganizationTableFactory } from '../audit';
3
+ import type { TypedSupabaseClient } from '../client';
4
+ export * from './dto';
5
+ /** What `create` writes: the secret already hashed, and the staff member who issued it. */
6
+ export type CreateUserApiCredentialRow = {
7
+ user_id: string;
8
+ organization_id: string;
9
+ client_id: string;
10
+ secret_hash: string;
11
+ label: string;
12
+ expires_at: string | null;
13
+ created_by: string;
14
+ };
15
+ /** The whole row, the secret hash included - what a mint is checked against. */
16
+ export type UserApiCredentialRow = UserApiCredentialEntity & {
17
+ secret_hash: string;
18
+ };
19
+ /**
20
+ * Credentials as staff read them, on their own client, through the column grant that leaves the
21
+ * secret hash out. Tenant users have no grant at all.
22
+ */
23
+ export declare class UserApiCredentialsReads {
24
+ private readonly supabase;
25
+ constructor(supabase: TypedSupabaseClient);
26
+ /** A user's credentials, newest first. */
27
+ listForUser(userId: string): Promise<UserApiCredentialEntity[]>;
28
+ }
29
+ /**
30
+ * The writes and the secret check, on the service client only: every write is the service
31
+ * role's, and nobody else may read the hash. A credential's writes are audited against its own
32
+ * organization as the system - the staff member who issued or revoked it is on the row itself
33
+ * (`created_by`, `revoked_by`), and is not a user an audit row can name.
34
+ */
35
+ export declare class UserApiCredentialsResource {
36
+ private readonly supabase;
37
+ private readonly tableFor;
38
+ constructor(supabase: TypedSupabaseClient, tableFor: OrganizationTableFactory);
39
+ /** Issues a credential. Its `secret_hash` is redacted in the audit row and never returned. */
40
+ create(row: CreateUserApiCredentialRow): Promise<UserApiCredentialEntity>;
41
+ /** The credential a client id names, with its secret hash. Null when none does. */
42
+ findByClientId(clientId: string): Promise<UserApiCredentialRow | null>;
43
+ /** Revokes a live credential. Null when it was revoked already. */
44
+ revoke(credentialId: string, revokedBy: string): Promise<UserApiCredentialEntity | null>;
45
+ /** Stamps a successful mint, unaudited: when it was last used, not a change to the credential. */
46
+ markUsed(credentialId: string): Promise<void>;
47
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.UserApiCredentialsResource = exports.UserApiCredentialsReads = void 0;
18
+ const audit_1 = require("../audit");
19
+ const errors_1 = require("../errors");
20
+ __exportStar(require("./dto"), exports);
21
+ // Exactly the columns the staff grant covers: never `secret_hash`, which only the mint reads.
22
+ const CREDENTIAL_SELECT = 'mms_id, user_id, organization_id, client_id, label, created_at, created_by, expires_at, revoked_at, revoked_by, last_used_at';
23
+ /** A written row as a reader sees it: without the secret hash. */
24
+ const toCredential = (row) => {
25
+ const { secret_hash: _secretHash, ...credential } = row;
26
+ return credential;
27
+ };
28
+ /**
29
+ * Credentials as staff read them, on their own client, through the column grant that leaves the
30
+ * secret hash out. Tenant users have no grant at all.
31
+ */
32
+ class UserApiCredentialsReads {
33
+ supabase;
34
+ constructor(supabase) {
35
+ this.supabase = supabase;
36
+ }
37
+ /** A user's credentials, newest first. */
38
+ async listForUser(userId) {
39
+ const { data, error } = await this.supabase
40
+ .from('user_api_credentials')
41
+ .select(CREDENTIAL_SELECT)
42
+ .eq('user_id', userId)
43
+ .order('created_at', { ascending: false });
44
+ if (error)
45
+ throw (0, errors_1.mapPostgrestError)(error);
46
+ return (data ?? []);
47
+ }
48
+ }
49
+ exports.UserApiCredentialsReads = UserApiCredentialsReads;
50
+ /**
51
+ * The writes and the secret check, on the service client only: every write is the service
52
+ * role's, and nobody else may read the hash. A credential's writes are audited against its own
53
+ * organization as the system - the staff member who issued or revoked it is on the row itself
54
+ * (`created_by`, `revoked_by`), and is not a user an audit row can name.
55
+ */
56
+ class UserApiCredentialsResource {
57
+ supabase;
58
+ tableFor;
59
+ constructor(supabase, tableFor) {
60
+ this.supabase = supabase;
61
+ this.tableFor = tableFor;
62
+ }
63
+ /** Issues a credential. Its `secret_hash` is redacted in the audit row and never returned. */
64
+ async create(row) {
65
+ return toCredential(await this.tableFor(row.organization_id).insert(row));
66
+ }
67
+ /** The credential a client id names, with its secret hash. Null when none does. */
68
+ async findByClientId(clientId) {
69
+ const { data, error } = await this.supabase
70
+ .from('user_api_credentials')
71
+ .select('*')
72
+ .eq('client_id', clientId)
73
+ .maybeSingle();
74
+ if (error)
75
+ throw (0, errors_1.mapPostgrestError)(error);
76
+ return data ? (0, audit_1.withoutInternalId)(data) : null;
77
+ }
78
+ /** Revokes a live credential. Null when it was revoked already. */
79
+ async revoke(credentialId, revokedBy) {
80
+ // The audited table is the credential's organization's, so that is read first.
81
+ const { data, error } = await this.supabase
82
+ .from('user_api_credentials')
83
+ .select('organization_id')
84
+ .eq('mms_id', credentialId)
85
+ .single();
86
+ if (error)
87
+ throw (0, errors_1.mapPostgrestError)(error);
88
+ const row = await this.tableFor(data.organization_id).updateWhere(credentialId, (query) => query.is('revoked_at', null), { revoked_at: new Date().toISOString(), revoked_by: revokedBy });
89
+ return row ? toCredential(row) : null;
90
+ }
91
+ /** Stamps a successful mint, unaudited: when it was last used, not a change to the credential. */
92
+ async markUsed(credentialId) {
93
+ const { error } = await this.supabase
94
+ .from('user_api_credentials')
95
+ .update({ last_used_at: new Date().toISOString() })
96
+ .eq('mms_id', credentialId);
97
+ if (error)
98
+ throw (0, errors_1.mapPostgrestError)(error);
99
+ }
100
+ }
101
+ exports.UserApiCredentialsResource = UserApiCredentialsResource;
@@ -1,4 +1,4 @@
1
- import type { CreateUserDto as CreateUserDtoType, GetUsersDto as GetUsersDtoType, UpdateMyProfileDto as UpdateMyProfileDtoType, UpdateUserDto as UpdateUserDtoType, UserIdDto as UserIdDtoType } from '@managemint-solutions/entities/users/dto';
1
+ import type { CreateUserDto as CreateUserDtoType, GetUsersDto as GetUsersDtoType, StaffGetUsersDto as StaffGetUsersDtoType, UpdateMyProfileDto as UpdateMyProfileDtoType, UpdateUserDto as UpdateUserDtoType, UserIdDto as UserIdDtoType } from '@managemint-solutions/entities/users/dto';
2
2
  import { EmergencyContactRelationship, EmploymentType } from '@managemint-solutions/entities/users/enum';
3
3
  export declare class UserIdDto implements UserIdDtoType {
4
4
  readonly userId: string;
@@ -28,6 +28,10 @@ export declare class GetUsersQueryDto implements Omit<GetUsersDtoType, 'active_i
28
28
  readonly active_in?: boolean[] | null;
29
29
  readonly manager_id?: string | null;
30
30
  }
31
+ /** A staff read across organizations, optionally narrowed to one of them. */
32
+ export declare class StaffGetUsersDto extends GetUsersQueryDto implements Omit<StaffGetUsersDtoType, 'active_in'> {
33
+ readonly organization_id?: string | null;
34
+ }
31
35
  export declare class UpdateUserDto implements UpdateUserDtoType {
32
36
  readonly name: string;
33
37
  readonly surname: string;
package/dist/users/dto.js CHANGED
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.UpdateMyProfileDto = exports.UpdateUserDto = exports.GetUsersQueryDto = exports.AddUserDto = exports.UserIdDto = void 0;
12
+ exports.UpdateMyProfileDto = exports.UpdateUserDto = exports.StaffGetUsersDto = exports.GetUsersQueryDto = exports.AddUserDto = exports.UserIdDto = void 0;
13
13
  const class_validator_1 = require("class-validator");
14
14
  const class_transformer_1 = require("class-transformer");
15
15
  const validators_1 = require("../validators");
@@ -51,7 +51,7 @@ __decorate([
51
51
  __decorate([
52
52
  (0, class_validator_1.IsString)({ message: 'Password is required' }),
53
53
  (0, class_validator_1.MinLength)(8, { message: 'Password must be at least 8 characters' }),
54
- (0, class_validator_1.Matches)(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*])/, { message: 'Password must contain at least 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character (!@#$%^&*)' }),
54
+ (0, validators_1.MatchesPasswordRules)(),
55
55
  __metadata("design:type", String)
56
56
  ], AddUserDto.prototype, "password", void 0);
57
57
  __decorate([
@@ -134,6 +134,17 @@ __decorate([
134
134
  (0, class_validator_1.IsUUID)('4', { message: 'Invalid manager ID format' }),
135
135
  __metadata("design:type", Object)
136
136
  ], GetUsersQueryDto.prototype, "manager_id", void 0);
137
+ /** A staff read across organizations, optionally narrowed to one of them. */
138
+ class StaffGetUsersDto extends GetUsersQueryDto {
139
+ organization_id;
140
+ }
141
+ exports.StaffGetUsersDto = StaffGetUsersDto;
142
+ __decorate([
143
+ (0, class_validator_1.IsOptional)(),
144
+ (0, validators_1.IsNullable)(),
145
+ (0, class_validator_1.IsUUID)('7', { message: 'Invalid organization ID format' }),
146
+ __metadata("design:type", Object)
147
+ ], StaffGetUsersDto.prototype, "organization_id", void 0);
137
148
  class UpdateUserDto {
138
149
  name;
139
150
  surname;
@@ -1,6 +1,6 @@
1
1
  import type { MmsList } from '@managemint-solutions/entities/common';
2
2
  import type { UserIdentity, UserSummary } from '@managemint-solutions/entities/users';
3
- import { type AuditActor, type AuditResource, type AuditRow } from '../audit';
3
+ import { type AuditResource, type AuditRow, EntityAuditTrail, type ResourceActor } from '../audit';
4
4
  import type { TypedSupabaseClient } from '../client';
5
5
  import type { GetUsersQueryDto } from './dto';
6
6
  export * from './dto';
@@ -24,8 +24,12 @@ export declare class UsersResource {
24
24
  private readonly supabase;
25
25
  private readonly auth;
26
26
  private readonly table;
27
- constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
28
- list(query: GetUsersQueryDto): Promise<MmsList<UserSummary>>;
27
+ /** This record's history - its own rows and the rows that hang off it. */
28
+ readonly audit: EntityAuditTrail;
29
+ constructor(supabase: TypedSupabaseClient, auth: ResourceActor, audit: AuditResource);
30
+ list(query: GetUsersQueryDto & {
31
+ organization_id?: string | null;
32
+ }): Promise<MmsList<UserSummary>>;
29
33
  getRow(userId: string): Promise<UserRow>;
30
34
  /**
31
35
  * The public profile behind a `*_by` / `manager_id` column. `user_profiles` is a