@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.
- package/README.md +92 -13
- package/dist/additional-details/index.d.ts +2 -2
- package/dist/additional-details/index.js +6 -10
- package/dist/audit/dto.d.ts +3 -8
- package/dist/audit/dto.js +7 -48
- package/dist/audit/index.d.ts +71 -10
- package/dist/audit/index.js +234 -55
- package/dist/auth-client/index.d.ts +2 -0
- package/dist/auth-client/index.js +12 -0
- package/dist/auth.d.ts +21 -0
- package/dist/auth.js +47 -3
- package/dist/clients/dto.d.ts +5 -1
- package/dist/clients/dto.js +12 -1
- package/dist/clients/index.d.ts +8 -4
- package/dist/clients/index.js +11 -8
- package/dist/emulation-handshakes/index.d.ts +13 -0
- package/dist/emulation-handshakes/index.js +36 -0
- package/dist/errors.js +2 -2
- package/dist/index.d.ts +14 -10
- package/dist/index.js +23 -4
- package/dist/leads/dto.d.ts +5 -1
- package/dist/leads/dto.js +12 -1
- package/dist/leads/index.d.ts +8 -5
- package/dist/leads/index.js +23 -21
- package/dist/leave/leave-adjustments.js +1 -0
- package/dist/leave/leave-entitlements.d.ts +3 -1
- package/dist/leave/leave-entitlements.js +3 -0
- package/dist/leave/leave-requests.d.ts +3 -1
- package/dist/leave/leave-requests.js +3 -0
- package/dist/nest/index.d.ts +26 -1
- package/dist/nest/index.js +58 -1
- package/dist/notifications/index.d.ts +1 -3
- package/dist/organization-payment-methods/index.d.ts +5 -6
- package/dist/organization-payment-methods/index.js +4 -17
- package/dist/organizations/dto.d.ts +9 -1
- package/dist/organizations/dto.js +40 -1
- package/dist/organizations/index.d.ts +13 -3
- package/dist/organizations/index.js +59 -3
- package/dist/permissions/index.d.ts +6 -4
- package/dist/permissions/index.js +19 -12
- package/dist/projects/dto.d.ts +5 -1
- package/dist/projects/dto.js +12 -1
- package/dist/projects/index.d.ts +8 -4
- package/dist/projects/index.js +11 -8
- package/dist/service-client/index.d.ts +39 -1
- package/dist/service-client/index.js +89 -2
- package/dist/staff/dto.d.ts +14 -0
- package/dist/staff/dto.js +63 -0
- package/dist/staff/index.d.ts +50 -0
- package/dist/staff/index.js +139 -0
- package/dist/staff-client.d.ts +37 -0
- package/dist/staff-client.js +52 -0
- package/dist/tasks/dto.d.ts +5 -1
- package/dist/tasks/dto.js +12 -1
- package/dist/tasks/index.d.ts +15 -10
- package/dist/tasks/index.js +34 -31
- package/dist/tenancy.d.ts +13 -0
- package/dist/tenancy.js +25 -0
- package/dist/timesheets/timesheet-entries.js +1 -0
- package/dist/timesheets/timesheets.d.ts +3 -1
- package/dist/timesheets/timesheets.js +3 -0
- package/dist/user-api-credentials/dto.d.ts +12 -0
- package/dist/user-api-credentials/dto.js +56 -0
- package/dist/user-api-credentials/index.d.ts +47 -0
- package/dist/user-api-credentials/index.js +101 -0
- package/dist/users/dto.d.ts +5 -1
- package/dist/users/dto.js +13 -2
- package/dist/users/index.d.ts +7 -3
- package/dist/users/index.js +16 -29
- package/dist/validators.d.ts +2 -0
- package/dist/validators.js +6 -1
- package/package.json +2 -2
package/dist/tasks/index.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
11
|
+
private readonly auditLog;
|
|
12
12
|
private readonly table;
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
47
|
-
* only when the set actually moved -
|
|
48
|
-
*
|
|
49
|
-
* deleted, so should the trigger still refuse one (a label
|
|
50
|
-
* task's existing labels are untouched, and a label kept
|
|
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
|
}
|
package/dist/tasks/index.js
CHANGED
|
@@ -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
|
-
|
|
128
|
+
auditLog;
|
|
128
129
|
table;
|
|
129
|
-
|
|
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.
|
|
133
|
-
this.table = (0, audit_1.auditedTable)(supabase, auth,
|
|
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
|
|
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',
|
|
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',
|
|
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:
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
425
|
-
* only when the set actually moved -
|
|
426
|
-
*
|
|
427
|
-
* deleted, so should the trigger still refuse one (a label
|
|
428
|
-
* task's existing labels are untouched, and a label kept
|
|
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',
|
|
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:
|
|
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',
|
|
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.
|
|
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;
|
package/dist/tenancy.js
ADDED
|
@@ -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;
|
package/dist/users/dto.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
package/dist/users/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
28
|
-
|
|
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
|