@managemint-solutions/sdk 0.53.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 +75 -3
- package/dist/additional-details/index.d.ts +2 -2
- package/dist/additional-details/index.js +6 -10
- package/dist/audit/index.d.ts +11 -5
- package/dist/audit/index.js +20 -12
- 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 +5 -3
- package/dist/clients/index.js +8 -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 +13 -9
- package/dist/index.js +22 -4
- package/dist/leads/dto.d.ts +5 -1
- package/dist/leads/dto.js +12 -1
- package/dist/leads/index.d.ts +5 -3
- package/dist/leads/index.js +20 -19
- 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/organizations/dto.d.ts +9 -1
- package/dist/organizations/dto.js +40 -1
- package/dist/organizations/index.d.ts +11 -3
- package/dist/organizations/index.js +30 -2
- package/dist/permissions/index.d.ts +6 -4
- package/dist/permissions/index.js +17 -12
- package/dist/projects/dto.d.ts +5 -1
- package/dist/projects/dto.js +12 -1
- package/dist/projects/index.d.ts +5 -3
- package/dist/projects/index.js +8 -8
- package/dist/service-client/index.d.ts +32 -1
- package/dist/service-client/index.js +54 -1
- 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 +5 -3
- package/dist/tasks/index.js +18 -19
- package/dist/tenancy.d.ts +13 -0
- package/dist/tenancy.js +25 -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 +5 -3
- package/dist/users/index.js +13 -29
- package/dist/validators.d.ts +2 -0
- package/dist/validators.js +6 -1
- package/package.json +2 -2
|
@@ -0,0 +1,139 @@
|
|
|
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.StaffPermissionsResource = exports.StaffResource = void 0;
|
|
18
|
+
const audit_1 = require("../audit");
|
|
19
|
+
const errors_1 = require("../errors");
|
|
20
|
+
const permissions_1 = require("../permissions");
|
|
21
|
+
__exportStar(require("./dto"), exports);
|
|
22
|
+
const STAFF_SELECT = 'mms_id, email, name, active, created_at, created_by, deactivated_at, deactivated_by';
|
|
23
|
+
/**
|
|
24
|
+
* The `staff` table. Staff read it on their own client; every write is the service role's (staff
|
|
25
|
+
* hold SELECT only), so `preRegister`, `removePending` and `setActive` belong to the service
|
|
26
|
+
* client. Unaudited: an `audit_logs` row belongs to an organization, and staff have none.
|
|
27
|
+
*/
|
|
28
|
+
class StaffResource {
|
|
29
|
+
supabase;
|
|
30
|
+
constructor(supabase) {
|
|
31
|
+
this.supabase = supabase;
|
|
32
|
+
}
|
|
33
|
+
/** Every staff member, by name. A pending pre-registration is not one yet. */
|
|
34
|
+
async list() {
|
|
35
|
+
const { data, error } = await this.supabase
|
|
36
|
+
.from('staff')
|
|
37
|
+
.select(STAFF_SELECT)
|
|
38
|
+
.not('mms_id', 'is', null)
|
|
39
|
+
.order('name');
|
|
40
|
+
if (error)
|
|
41
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
42
|
+
return (data ?? []);
|
|
43
|
+
}
|
|
44
|
+
async getById(staffId) {
|
|
45
|
+
const { data, error } = await this.supabase
|
|
46
|
+
.from('staff')
|
|
47
|
+
.select(STAFF_SELECT)
|
|
48
|
+
.eq('mms_id', staffId)
|
|
49
|
+
.single();
|
|
50
|
+
if (error)
|
|
51
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The address, written before its auth user exists - the first half of creating a staff
|
|
56
|
+
* member. `mms_id` stays null until GoTrue creates the auth user with the `staff` role and
|
|
57
|
+
* `handle_auth_user_role_set` links the two.
|
|
58
|
+
*/
|
|
59
|
+
async preRegister(input) {
|
|
60
|
+
const { error } = await this.supabase.from('staff').insert(input);
|
|
61
|
+
if (error)
|
|
62
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
63
|
+
}
|
|
64
|
+
/** Undoes a pre-registration whose auth user was never created. A linked row is never touched. */
|
|
65
|
+
async removePending(email) {
|
|
66
|
+
const { error } = await this.supabase
|
|
67
|
+
.from('staff')
|
|
68
|
+
.delete()
|
|
69
|
+
.eq('email', email)
|
|
70
|
+
.is('mms_id', null);
|
|
71
|
+
if (error)
|
|
72
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
73
|
+
}
|
|
74
|
+
/** Deactivates, stamping who and when, or reactivates, clearing them. */
|
|
75
|
+
async setActive(staffId, active, by) {
|
|
76
|
+
const { data, error } = await this.supabase
|
|
77
|
+
.from('staff')
|
|
78
|
+
.update({
|
|
79
|
+
active,
|
|
80
|
+
deactivated_at: active ? null : new Date().toISOString(),
|
|
81
|
+
deactivated_by: active ? null : by,
|
|
82
|
+
})
|
|
83
|
+
.eq('mms_id', staffId)
|
|
84
|
+
.select(STAFF_SELECT)
|
|
85
|
+
.single();
|
|
86
|
+
if (error)
|
|
87
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.StaffResource = StaffResource;
|
|
92
|
+
/**
|
|
93
|
+
* The `staff_permissions` table: one row per staff member, made by the role trigger alongside
|
|
94
|
+
* the link. Staff read it on their own client; `updateForStaff` is the service client's.
|
|
95
|
+
*/
|
|
96
|
+
class StaffPermissionsResource {
|
|
97
|
+
supabase;
|
|
98
|
+
constructor(supabase) {
|
|
99
|
+
this.supabase = supabase;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The one lookup the admin api's guard makes per gated request: the flag the route asked for,
|
|
103
|
+
* plus the admin bypass. Null when there is no row, which the guard treats as a denial.
|
|
104
|
+
*/
|
|
105
|
+
async flagsFor(staffId, flag) {
|
|
106
|
+
(0, permissions_1.checkPermissionFlag)(flag);
|
|
107
|
+
const { data, error } = await this.supabase
|
|
108
|
+
.from('staff_permissions')
|
|
109
|
+
.select(`is_admin, ${flag}`)
|
|
110
|
+
.eq('staff_id', staffId)
|
|
111
|
+
.maybeSingle();
|
|
112
|
+
if (error)
|
|
113
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
114
|
+
return (data ?? null);
|
|
115
|
+
}
|
|
116
|
+
/** The whole row. `*`: every flag, so a new flag column needs no select edit. */
|
|
117
|
+
async getForStaff(staffId) {
|
|
118
|
+
const { data, error } = await this.supabase
|
|
119
|
+
.from('staff_permissions')
|
|
120
|
+
.select('*')
|
|
121
|
+
.eq('staff_id', staffId)
|
|
122
|
+
.single();
|
|
123
|
+
if (error)
|
|
124
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
125
|
+
return (0, audit_1.withoutInternalId)(data);
|
|
126
|
+
}
|
|
127
|
+
async updateForStaff(staffId, patch, by) {
|
|
128
|
+
const { data, error } = await this.supabase
|
|
129
|
+
.from('staff_permissions')
|
|
130
|
+
.update({ ...patch, updated_at: new Date().toISOString(), last_updated_by: by })
|
|
131
|
+
.eq('staff_id', staffId)
|
|
132
|
+
.select('*')
|
|
133
|
+
.single();
|
|
134
|
+
if (error)
|
|
135
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
136
|
+
return (0, audit_1.withoutInternalId)(data);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.StaffPermissionsResource = StaffPermissionsResource;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type StaffContext } from './auth';
|
|
2
|
+
import { AuditResource } from './audit';
|
|
3
|
+
import type { SupabaseClientConfig, TypedSupabaseClient } from './client';
|
|
4
|
+
import { ClientsResource } from './clients';
|
|
5
|
+
import { LeadsResource } from './leads';
|
|
6
|
+
import { OrganizationsResource } from './organizations';
|
|
7
|
+
import { ProjectsResource } from './projects';
|
|
8
|
+
import { StaffPermissionsResource, StaffResource } from './staff';
|
|
9
|
+
import { TasksResource } from './tasks';
|
|
10
|
+
import { UserApiCredentialsReads } from './user-api-credentials';
|
|
11
|
+
import { UsersResource } from './users';
|
|
12
|
+
type SupabaseStaffClientConfig = Omit<SupabaseClientConfig, 'auth' | 'onAuditError'> & {
|
|
13
|
+
auth: StaffContext;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A staff member's data client, for the admin api: every query carries the staff token, so the
|
|
17
|
+
* `staff` role's policies decide what comes back. Reads span every organization and every write
|
|
18
|
+
* is refused with a 403. A resource not listed here is simply not available to staff.
|
|
19
|
+
*/
|
|
20
|
+
export declare class SupabaseStaffClient {
|
|
21
|
+
readonly supabase: TypedSupabaseClient;
|
|
22
|
+
/** The staff member the token belongs to - the caller, for a service that records who acted. */
|
|
23
|
+
readonly auth: StaffContext;
|
|
24
|
+
readonly audit: AuditResource;
|
|
25
|
+
readonly clients: ClientsResource;
|
|
26
|
+
readonly leads: LeadsResource;
|
|
27
|
+
readonly projects: ProjectsResource;
|
|
28
|
+
readonly tasks: TasksResource;
|
|
29
|
+
readonly users: UsersResource;
|
|
30
|
+
readonly organizations: OrganizationsResource;
|
|
31
|
+
readonly staff: StaffResource;
|
|
32
|
+
readonly staffPermissions: StaffPermissionsResource;
|
|
33
|
+
readonly userApiCredentials: UserApiCredentialsReads;
|
|
34
|
+
constructor({ url, key, accessToken, auth, client }: SupabaseStaffClientConfig);
|
|
35
|
+
}
|
|
36
|
+
export declare const createSupabaseStaffClient: (config: SupabaseStaffClientConfig) => SupabaseStaffClient;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSupabaseStaffClient = exports.SupabaseStaffClient = void 0;
|
|
4
|
+
const auth_1 = require("./auth");
|
|
5
|
+
const audit_1 = require("./audit");
|
|
6
|
+
const additional_details_1 = require("./additional-details");
|
|
7
|
+
const clients_1 = require("./clients");
|
|
8
|
+
const leads_1 = require("./leads");
|
|
9
|
+
const organizations_1 = require("./organizations");
|
|
10
|
+
const projects_1 = require("./projects");
|
|
11
|
+
const staff_1 = require("./staff");
|
|
12
|
+
const tasks_1 = require("./tasks");
|
|
13
|
+
const user_api_credentials_1 = require("./user-api-credentials");
|
|
14
|
+
const users_1 = require("./users");
|
|
15
|
+
/**
|
|
16
|
+
* A staff member's data client, for the admin api: every query carries the staff token, so the
|
|
17
|
+
* `staff` role's policies decide what comes back. Reads span every organization and every write
|
|
18
|
+
* is refused with a 403. A resource not listed here is simply not available to staff.
|
|
19
|
+
*/
|
|
20
|
+
class SupabaseStaffClient {
|
|
21
|
+
supabase; // escape hatch for not-yet-migrated queries
|
|
22
|
+
/** The staff member the token belongs to - the caller, for a service that records who acted. */
|
|
23
|
+
auth;
|
|
24
|
+
audit;
|
|
25
|
+
clients;
|
|
26
|
+
leads;
|
|
27
|
+
projects;
|
|
28
|
+
tasks;
|
|
29
|
+
users;
|
|
30
|
+
organizations;
|
|
31
|
+
staff;
|
|
32
|
+
staffPermissions;
|
|
33
|
+
userApiCredentials;
|
|
34
|
+
constructor({ url, key, accessToken, auth, client }) {
|
|
35
|
+
this.supabase = client ?? (0, auth_1.createRawClient)(url, key, accessToken);
|
|
36
|
+
this.auth = auth;
|
|
37
|
+
this.audit = new audit_1.AuditResource(this.supabase, auth);
|
|
38
|
+
this.clients = new clients_1.ClientsResource(this.supabase, auth, this.audit);
|
|
39
|
+
const additionalDetails = new additional_details_1.AdditionalDetailsResource(this.supabase, auth, this.audit);
|
|
40
|
+
this.leads = new leads_1.LeadsResource(this.supabase, auth, this.audit, additionalDetails);
|
|
41
|
+
this.projects = new projects_1.ProjectsResource(this.supabase, auth, this.audit);
|
|
42
|
+
this.tasks = new tasks_1.TasksResource(this.supabase, auth, this.audit);
|
|
43
|
+
this.users = new users_1.UsersResource(this.supabase, auth, this.audit);
|
|
44
|
+
this.organizations = new organizations_1.OrganizationsResource(this.supabase, auth, this.audit);
|
|
45
|
+
this.staff = new staff_1.StaffResource(this.supabase);
|
|
46
|
+
this.staffPermissions = new staff_1.StaffPermissionsResource(this.supabase);
|
|
47
|
+
this.userApiCredentials = new user_api_credentials_1.UserApiCredentialsReads(this.supabase);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.SupabaseStaffClient = SupabaseStaffClient;
|
|
51
|
+
const createSupabaseStaffClient = (config) => new SupabaseStaffClient(config);
|
|
52
|
+
exports.createSupabaseStaffClient = createSupabaseStaffClient;
|
package/dist/tasks/dto.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AddTaskLinkDto as AddTaskLinkDtoType, CreateTaskDto as CreateTaskDtoType, GetTasksDto as GetTasksDtoType, TaskIdDto as TaskIdDtoType, TaskLinkIdDto as TaskLinkIdDtoType, UpdateTaskDto as UpdateTaskDtoType } from '@managemint-solutions/entities/tasks/dto';
|
|
1
|
+
import type { AddTaskLinkDto as AddTaskLinkDtoType, CreateTaskDto as CreateTaskDtoType, GetTasksDto as GetTasksDtoType, StaffGetTasksDto as StaffGetTasksDtoType, TaskIdDto as TaskIdDtoType, TaskLinkIdDto as TaskLinkIdDtoType, UpdateTaskDto as UpdateTaskDtoType } from '@managemint-solutions/entities/tasks/dto';
|
|
2
2
|
import { TaskLinkType } from '@managemint-solutions/entities/tasks/enum';
|
|
3
3
|
export declare class TaskIdDto implements TaskIdDtoType {
|
|
4
4
|
readonly taskId: string;
|
|
@@ -31,6 +31,10 @@ export declare class GetTasksDto implements Omit<GetTasksDtoType, 'status_id_in'
|
|
|
31
31
|
readonly priority_in?: number[] | null;
|
|
32
32
|
readonly archive: boolean;
|
|
33
33
|
}
|
|
34
|
+
/** A staff read across organizations, optionally narrowed to one of them. */
|
|
35
|
+
export declare class StaffGetTasksDto extends GetTasksDto implements Omit<StaffGetTasksDtoType, 'status_id_in' | 'status_id_not_in' | 'label_id_in' | 'priority_in'> {
|
|
36
|
+
readonly organization_id?: string | null;
|
|
37
|
+
}
|
|
34
38
|
export declare class CreateTaskDto implements CreateTaskDtoType {
|
|
35
39
|
readonly name: string;
|
|
36
40
|
readonly description?: string | null;
|
package/dist/tasks/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.UpdateTaskDto = exports.CreateTaskDto = exports.GetTasksDto = exports.AddTaskLinkDto = exports.TaskLinkIdDto = exports.TaskIdDto = void 0;
|
|
12
|
+
exports.UpdateTaskDto = exports.CreateTaskDto = exports.StaffGetTasksDto = exports.GetTasksDto = exports.AddTaskLinkDto = exports.TaskLinkIdDto = exports.TaskIdDto = void 0;
|
|
13
13
|
const class_validator_1 = require("class-validator");
|
|
14
14
|
const class_transformer_1 = require("class-transformer");
|
|
15
15
|
const enum_1 = require("@managemint-solutions/entities/tasks/enum");
|
|
@@ -206,6 +206,17 @@ __decorate([
|
|
|
206
206
|
(0, class_validator_1.IsBoolean)({ message: 'Include archived must be a boolean' }),
|
|
207
207
|
__metadata("design:type", Boolean)
|
|
208
208
|
], GetTasksDto.prototype, "archive", void 0);
|
|
209
|
+
/** A staff read across organizations, optionally narrowed to one of them. */
|
|
210
|
+
class StaffGetTasksDto extends GetTasksDto {
|
|
211
|
+
organization_id;
|
|
212
|
+
}
|
|
213
|
+
exports.StaffGetTasksDto = StaffGetTasksDto;
|
|
214
|
+
__decorate([
|
|
215
|
+
(0, class_validator_1.IsOptional)(),
|
|
216
|
+
(0, validators_1.IsNullable)(),
|
|
217
|
+
(0, class_validator_1.IsUUID)('7', { message: 'Invalid organization ID format' }),
|
|
218
|
+
__metadata("design:type", Object)
|
|
219
|
+
], StaffGetTasksDto.prototype, "organization_id", void 0);
|
|
209
220
|
class CreateTaskDto {
|
|
210
221
|
name;
|
|
211
222
|
description;
|
package/dist/tasks/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MmsList } from '@managemint-solutions/entities/common';
|
|
2
2
|
import type { TaskEntity, TaskSummary } from '@managemint-solutions/entities/tasks';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ClientContext } from '../auth';
|
|
4
4
|
import { EntityAuditTrail, type AuditResource } from '../audit';
|
|
5
5
|
import type { TypedSupabaseClient } from '../client';
|
|
6
6
|
import type { AddTaskLinkDto, CreateTaskDto, GetTasksDto, UpdateTaskDto } from './dto';
|
|
@@ -12,8 +12,10 @@ export declare class TasksResource {
|
|
|
12
12
|
private readonly table;
|
|
13
13
|
/** This record's history - its own rows and the rows that hang off it. */
|
|
14
14
|
readonly audit: EntityAuditTrail;
|
|
15
|
-
constructor(supabase: TypedSupabaseClient, auth:
|
|
16
|
-
list(query: GetTasksDto
|
|
15
|
+
constructor(supabase: TypedSupabaseClient, auth: ClientContext, auditLog: AuditResource);
|
|
16
|
+
list(query: GetTasksDto & {
|
|
17
|
+
organization_id?: string | null;
|
|
18
|
+
}): Promise<MmsList<TaskSummary>>;
|
|
17
19
|
getById(taskId: string): Promise<TaskEntity>;
|
|
18
20
|
create(input: CreateTaskDto): Promise<TaskEntity>;
|
|
19
21
|
update(taskId: string, input: UpdateTaskDto): Promise<TaskEntity>;
|
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;
|
|
@@ -146,17 +147,17 @@ class TasksResource {
|
|
|
146
147
|
const labelIdIn = query.label_id_in && query.label_id_in.length > 0 ? query.label_id_in : null;
|
|
147
148
|
// `order`/`range` return `this`, so the builder stays a filter builder and the conditional
|
|
148
149
|
// filters below can be reassigned onto it without a cast.
|
|
149
|
-
let tasksQuery = this.supabase
|
|
150
|
+
let tasksQuery = (0, tenancy_1.scopeToOrganization)(this.supabase
|
|
150
151
|
.from('tasks')
|
|
151
|
-
.select(labelIdIn ? `${TASK_LIST_SELECT}, ${LABEL_FILTER_SELECT}` : TASK_LIST_SELECT, {
|
|
152
|
-
count: 'estimated',
|
|
153
|
-
})
|
|
154
|
-
.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)
|
|
155
153
|
.eq('archived', query.archive)
|
|
156
154
|
.eq('deleted', false)
|
|
157
155
|
.order('priority', { ascending: false })
|
|
158
156
|
.order('created_at', { ascending: false })
|
|
159
157
|
.range(offset, offset + pageSize - 1);
|
|
158
|
+
if (query.organization_id) {
|
|
159
|
+
tasksQuery = tasksQuery.eq('organization_id', query.organization_id);
|
|
160
|
+
}
|
|
160
161
|
if (trimmedSearch) {
|
|
161
162
|
// "TASK-12", "#12" or "12" also finds task number 12; anything else is a text search only.
|
|
162
163
|
const taskNumber = trimmedSearch.match(/^(?:[a-z]+-|#)?(\d+)$/i)?.[1];
|
|
@@ -220,11 +221,7 @@ class TasksResource {
|
|
|
220
221
|
return (0, transforms_1.toMmsList)(rows, page, pageSize, count);
|
|
221
222
|
}
|
|
222
223
|
async getById(taskId) {
|
|
223
|
-
const { data, error } = await this.supabase
|
|
224
|
-
.from('tasks')
|
|
225
|
-
.select(TASK_DETAIL_SELECT)
|
|
226
|
-
.eq('mms_id', taskId)
|
|
227
|
-
.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)
|
|
228
225
|
.order('created_at', { referencedTable: 'links_out' })
|
|
229
226
|
.order('created_at', { referencedTable: 'links_in' })
|
|
230
227
|
.single();
|
|
@@ -250,7 +247,7 @@ class TasksResource {
|
|
|
250
247
|
// BEFORE INSERT trigger fills it in when it arrives null.
|
|
251
248
|
const row = await this.table.insert({
|
|
252
249
|
...fields,
|
|
253
|
-
organization_id: this.auth
|
|
250
|
+
organization_id: (0, tenancy_1.requireOrganization)(this.auth),
|
|
254
251
|
created_by: this.auth.mms_id,
|
|
255
252
|
created_at: new Date().toISOString(),
|
|
256
253
|
});
|
|
@@ -300,6 +297,7 @@ class TasksResource {
|
|
|
300
297
|
* made from.
|
|
301
298
|
*/
|
|
302
299
|
async linkTask(taskId, input) {
|
|
300
|
+
const organizationId = (0, tenancy_1.requireOrganization)(this.auth);
|
|
303
301
|
const refuse = (status, message, cause) => new errors_1.SupabaseClientError({ status, error: 'Database Error', message }, { cause });
|
|
304
302
|
if (input.linked_task_id === taskId) {
|
|
305
303
|
throw refuse(400, 'A task cannot be linked to itself');
|
|
@@ -307,7 +305,7 @@ class TasksResource {
|
|
|
307
305
|
const { data: tasks, error: tasksError } = await this.supabase
|
|
308
306
|
.from('tasks')
|
|
309
307
|
.select('mms_id, task_number:task_id')
|
|
310
|
-
.eq('organization_id',
|
|
308
|
+
.eq('organization_id', organizationId)
|
|
311
309
|
.eq('deleted', false)
|
|
312
310
|
.in('mms_id', [taskId, input.linked_task_id]);
|
|
313
311
|
if (tasksError)
|
|
@@ -328,7 +326,7 @@ class TasksResource {
|
|
|
328
326
|
const { data: parentLink, error: parentError } = await this.supabase
|
|
329
327
|
.from('task_links')
|
|
330
328
|
.select('task_id')
|
|
331
|
-
.eq('organization_id',
|
|
329
|
+
.eq('organization_id', organizationId)
|
|
332
330
|
.eq('linked_task_id', child)
|
|
333
331
|
.eq('link_type', enum_1.TaskLinkType.CHILD)
|
|
334
332
|
.maybeSingle();
|
|
@@ -344,7 +342,7 @@ class TasksResource {
|
|
|
344
342
|
task_id: taskId,
|
|
345
343
|
linked_task_id: input.linked_task_id,
|
|
346
344
|
link_type: input.link_type,
|
|
347
|
-
organization_id:
|
|
345
|
+
organization_id: organizationId,
|
|
348
346
|
created_by: this.auth.mms_id,
|
|
349
347
|
});
|
|
350
348
|
if (error) {
|
|
@@ -376,7 +374,7 @@ class TasksResource {
|
|
|
376
374
|
const { data, error } = await this.supabase
|
|
377
375
|
.from('task_links')
|
|
378
376
|
.delete()
|
|
379
|
-
.eq('organization_id', this.auth
|
|
377
|
+
.eq('organization_id', (0, tenancy_1.requireOrganization)(this.auth))
|
|
380
378
|
.or(`and(task_id.eq.${taskId},linked_task_id.eq.${linkedTaskId}),and(task_id.eq.${linkedTaskId},linked_task_id.eq.${taskId})`)
|
|
381
379
|
.select('task_id, link_type, from:tasks!task_id ( task_number:task_id ), to:tasks!linked_task_id ( task_number:task_id )');
|
|
382
380
|
if (error)
|
|
@@ -409,7 +407,7 @@ class TasksResource {
|
|
|
409
407
|
const { count, error } = await this.supabase
|
|
410
408
|
.from('labels')
|
|
411
409
|
.select('mms_id', { count: 'exact', head: true })
|
|
412
|
-
.eq('organization_id', this.auth
|
|
410
|
+
.eq('organization_id', (0, tenancy_1.requireOrganization)(this.auth))
|
|
413
411
|
.eq('entity_type', enum_2.LabelEntityTypes.TASK)
|
|
414
412
|
.in('mms_id', labelIds);
|
|
415
413
|
if (error)
|
|
@@ -432,11 +430,12 @@ class TasksResource {
|
|
|
432
430
|
* across an edit keeps its place.
|
|
433
431
|
*/
|
|
434
432
|
async setLabels(taskId, labelIds) {
|
|
433
|
+
const organizationId = (0, tenancy_1.requireOrganization)(this.auth);
|
|
435
434
|
const { data: current, error: readError } = await this.supabase
|
|
436
435
|
.from('task_labels')
|
|
437
436
|
.select(PINNED_LABEL_SELECT)
|
|
438
437
|
.eq('task_id', taskId)
|
|
439
|
-
.eq('organization_id',
|
|
438
|
+
.eq('organization_id', organizationId)
|
|
440
439
|
.order('created_at');
|
|
441
440
|
if (readError)
|
|
442
441
|
throw (0, errors_1.mapPostgrestError)(readError);
|
|
@@ -453,7 +452,7 @@ class TasksResource {
|
|
|
453
452
|
.insert(added.map((labelId) => ({
|
|
454
453
|
task_id: taskId,
|
|
455
454
|
label_id: labelId,
|
|
456
|
-
organization_id:
|
|
455
|
+
organization_id: organizationId,
|
|
457
456
|
created_by: this.auth.mms_id,
|
|
458
457
|
})))
|
|
459
458
|
.select(PINNED_LABEL_SELECT);
|
|
@@ -466,7 +465,7 @@ class TasksResource {
|
|
|
466
465
|
.from('task_labels')
|
|
467
466
|
.delete()
|
|
468
467
|
.eq('task_id', taskId)
|
|
469
|
-
.eq('organization_id',
|
|
468
|
+
.eq('organization_id', organizationId)
|
|
470
469
|
.in('label_id', dropped.map((row) => row.label_id));
|
|
471
470
|
if (error)
|
|
472
471
|
throw (0, errors_1.mapPostgrestError)(error);
|
|
@@ -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;
|
|
@@ -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
|
+
}
|